@luxalgo/vela 0.5.3 → 0.6.0

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.
Files changed (47) hide show
  1. package/README.md +3 -2
  2. package/dist/{DataProvider-gnzv-dgc.d.cts → DataProvider-Ce6PuCzQ.d.cts} +10 -1
  3. package/dist/{DataProvider-DTOEXKTI.d.ts → DataProvider-DNx9ntBD.d.ts} +10 -1
  4. package/dist/{chunk-EY63XBFZ.js → chunk-7ROONW6H.js} +132 -23
  5. package/dist/{chunk-L5NTMWPP.js → chunk-CA3N3PYT.js} +785 -857
  6. package/dist/{chunk-LJLZR44Y.js → chunk-EMS6PO2T.js} +5 -7
  7. package/dist/chunk-OGRQW3M6.js +1328 -0
  8. package/dist/{chunk-P556H6EA.js → chunk-QWIEKZRE.js} +59 -34
  9. package/dist/{chunk-WJKLBYRM.js → chunk-ZNL2X6U2.js} +1 -1
  10. package/dist/{contributions-Ci_i3IN2.d.cts → contributions-DToMhiw3.d.ts} +9 -2
  11. package/dist/{contributions-b6AVrqqG.d.ts → contributions-Dlc7ZWZz.d.cts} +9 -2
  12. package/dist/{history-Ci3M0xLV.d.ts → history-BZADxpKx.d.cts} +5 -3
  13. package/dist/{history-Cf1lVsap.d.cts → history-BuHXJZJ2.d.ts} +5 -3
  14. package/dist/index.cjs +2761 -1597
  15. package/dist/index.d.cts +14 -6
  16. package/dist/index.d.ts +14 -6
  17. package/dist/index.js +4 -4
  18. package/dist/{options-XTBpbx0s.d.ts → options-BlQ00Fju.d.cts} +3 -1
  19. package/dist/{options-XTBpbx0s.d.cts → options-BlQ00Fju.d.ts} +3 -1
  20. package/dist/{plugin-DlidwMj6.d.cts → plugin-BNPLOQO1.d.cts} +3 -3
  21. package/dist/{plugin-DP6A4t4o.d.ts → plugin-BqjEa5r3.d.ts} +3 -3
  22. package/dist/plugin.cjs +4 -0
  23. package/dist/plugin.d.cts +4 -4
  24. package/dist/plugin.d.ts +4 -4
  25. package/dist/plugin.js +2 -2
  26. package/dist/providers/binance.d.cts +2 -2
  27. package/dist/providers/binance.d.ts +2 -2
  28. package/dist/providers/coinbase.d.cts +2 -2
  29. package/dist/providers/coinbase.d.ts +2 -2
  30. package/dist/providers/hyperliquid.d.cts +2 -2
  31. package/dist/providers/hyperliquid.d.ts +2 -2
  32. package/dist/ui.cjs +1318 -4
  33. package/dist/ui.d.cts +383 -2
  34. package/dist/ui.d.ts +383 -2
  35. package/dist/ui.js +3 -3
  36. package/dist/vela.global.js +3729 -2565
  37. package/dist/vela.global.min.js +315 -66
  38. package/dist/widget.cjs +2359 -1054
  39. package/dist/widget.d.cts +30 -16
  40. package/dist/widget.d.ts +30 -16
  41. package/dist/widget.js +37 -14
  42. package/dist/workspace.cjs +2259 -944
  43. package/dist/workspace.d.cts +10 -5
  44. package/dist/workspace.d.ts +10 -5
  45. package/dist/workspace.js +29 -8
  46. package/package.json +1 -1
  47. package/dist/chunk-ATPU5CI6.js +0 -81
package/dist/widget.cjs CHANGED
@@ -476,6 +476,10 @@ registerIcon(
476
476
  S('<path d="M8 1.8 14 5 8 8.2 2 5z"/><path d="M2 8l6 3.2L14 8" opacity="0.7"/><path d="M2 11l6 3.2 6-3.2" opacity="0.4"/>')
477
477
  );
478
478
  registerIcon("clock", S('<circle cx="8" cy="8" r="6.2"/><path d="M8 4.8V8l2.4 1.6"/>'));
479
+ registerIcon(
480
+ "calendar",
481
+ S('<rect x="2.2" y="3.2" width="11.6" height="10.6" rx="1.4"/><path d="M5.2 1.8v2.8M10.8 1.8v2.8M2.2 6.8h11.6"/>')
482
+ );
479
483
  registerIcon("datawindow", S('<rect x="1.8" y="2.5" width="12.4" height="11" rx="1.5"/><path d="M4.5 5.5h4M4.5 8h7M4.5 10.5h5.5"/>'));
480
484
  registerIcon("camera", S('<path d="M5.5 4 6.5 2.5h3L10.5 4h3A1 1 0 0 1 14.5 5v7.5a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"/><circle cx="8" cy="8.5" r="2.6"/>'));
481
485
  registerIcon(
@@ -6932,7 +6936,7 @@ var ProviderRegistry = class {
6932
6936
  /** Register (or replace) a provider; returns a promise that settles when its index is built. */
6933
6937
  register(rawName, provider) {
6934
6938
  const name = normName(rawName);
6935
- const entry = { name, provider, index: null, descriptors: [], settled: false, settle: Promise.resolve() };
6939
+ const entry = { name, provider, index: null, prefixIndex: null, byTicker: null, descriptors: [], settled: false, settle: Promise.resolve() };
6936
6940
  this.entries.set(name, entry);
6937
6941
  entry.settle = this.buildIndex(entry);
6938
6942
  queueMicrotask(() => this.notify(false));
@@ -6945,6 +6949,9 @@ var ProviderRegistry = class {
6945
6949
  const symbols = await entry.provider.listSymbols();
6946
6950
  entry.descriptors = symbols;
6947
6951
  entry.index = new Set(symbols.map((s) => normTicker(s.ticker)));
6952
+ entry.byTicker = new Map(symbols.map((s) => [normTicker(s.ticker), s]));
6953
+ entry.prefixIndex = /* @__PURE__ */ new Map();
6954
+ for (const s of symbols) if (s.prefix) entry.prefixIndex.set(`${normName(s.prefix)}:${normTicker(s.ticker)}`, s);
6948
6955
  }
6949
6956
  } catch {
6950
6957
  entry.index = null;
@@ -6980,14 +6987,25 @@ var ProviderRegistry = class {
6980
6987
  }
6981
6988
  /**
6982
6989
  * Resolve a symbol to `{ provider, ticker }`, or null when nothing can serve it
6983
- * yet. Explicit prefix → that provider (must be registered). Bare first
6984
- * indexed provider (registration order, `opts.default` first) that contains the
6985
- * ticker; `opts.lenient` additionally allows a sole registered provider before
6986
- * its index exists.
6990
+ * yet. Explicit prefix → that provider (must be registered), else a provider whose
6991
+ * index declares that LISTING prefix for that ticker (`NASDAQ:AAPL` routes to the
6992
+ * provider serving Nasdaq-listed AAPL TradingView-strict, so `NYSE:AAPL` resolves
6993
+ * to nothing rather than auto-correcting). Bare → first indexed provider
6994
+ * (registration order, `opts.default` first) that contains the ticker;
6995
+ * `opts.lenient` additionally allows a sole registered provider before its index
6996
+ * exists.
6987
6997
  */
6988
6998
  resolve(raw, opts = {}) {
6989
6999
  const { provider, ticker } = this.parse(raw);
6990
- if (provider) return this.entries.has(provider) ? { provider, ticker } : null;
7000
+ if (provider) {
7001
+ if (this.entries.has(provider)) return { provider, ticker };
7002
+ const key = `${provider}:${normTicker(ticker)}`;
7003
+ for (const name of this.candidateOrder(opts.default)) {
7004
+ const d = this.entries.get(name).prefixIndex?.get(key);
7005
+ if (d) return { provider: name, ticker: d.ticker };
7006
+ }
7007
+ return null;
7008
+ }
6991
7009
  const norm = normTicker(ticker);
6992
7010
  for (const name of this.candidateOrder(opts.default)) {
6993
7011
  if (this.entries.get(name).index?.has(norm)) return { provider: name, ticker };
@@ -7053,6 +7071,22 @@ var ProviderRegistry = class {
7053
7071
  }
7054
7072
  return names;
7055
7073
  }
7074
+ /**
7075
+ * The DISPLAY prefix for a resolved symbol — the descriptor's LISTING prefix when the
7076
+ * data declares one, else the provider name. This is the single seam every label
7077
+ * (legend chip, committed/persisted `PREFIX:TICKER` strings) derives from, which is
7078
+ * what makes a non-canonical spelling impossible to display: the form always comes
7079
+ * from the data, never from what was typed.
7080
+ */
7081
+ displayPrefixOf(resolved) {
7082
+ const d = this.entries.get(resolved.provider)?.byTicker?.get(normTicker(resolved.ticker));
7083
+ return d?.prefix ?? resolved.provider;
7084
+ }
7085
+ /** The canonical `PREFIX:TICKER` string for a resolved symbol (descriptor spellings). */
7086
+ canonicalSymbol(resolved) {
7087
+ const d = this.entries.get(resolved.provider)?.byTicker?.get(normTicker(resolved.ticker));
7088
+ return `${d?.prefix ?? resolved.provider}:${d?.ticker ?? resolved.ticker}`;
7089
+ }
7056
7090
  /** Indexed symbols for one provider (or all, concatenated) — for autocomplete. */
7057
7091
  symbolsOf(rawName) {
7058
7092
  if (rawName != null) return this.entries.get(normName(rawName))?.descriptors ?? [];
@@ -9191,6 +9225,20 @@ var MultiProviderFeed = class {
9191
9225
  resolveSymbol(raw) {
9192
9226
  return this.registry.resolve(raw, { default: this.primaryProvider });
9193
9227
  }
9228
+ /**
9229
+ * The DISPLAY prefix for `raw` — the descriptor's LISTING prefix when the data
9230
+ * declares one (`NASDAQ` for AAPL), else the resolved provider name. Null while
9231
+ * nothing resolves the symbol.
9232
+ */
9233
+ displayPrefix(raw) {
9234
+ const resolved = this.resolveSymbol(raw);
9235
+ return resolved ? this.registry.displayPrefixOf(resolved) : null;
9236
+ }
9237
+ /** The canonical `PREFIX:TICKER` form of `raw`, or null while unresolvable. */
9238
+ canonicalSymbol(raw) {
9239
+ const resolved = this.resolveSymbol(raw);
9240
+ return resolved ? this.registry.canonicalSymbol(resolved) : null;
9241
+ }
9194
9242
  /** The registered provider INSTANCE under `name` (undefined if unknown). */
9195
9243
  providerInstance(name) {
9196
9244
  return this.registry.get(name);
@@ -9396,6 +9444,17 @@ var DataControl = class {
9396
9444
  resolve(symbol) {
9397
9445
  return this.registry?.resolveSymbol(symbol) ?? null;
9398
9446
  }
9447
+ /**
9448
+ * The DISPLAY prefix for `symbol` — the listing venue its descriptor declares
9449
+ * (`NASDAQ` for AAPL) or the resolved provider name. Null while unresolvable.
9450
+ */
9451
+ displayPrefix(symbol) {
9452
+ return this.registry?.displayPrefix(symbol) ?? null;
9453
+ }
9454
+ /** The canonical `PREFIX:TICKER` form of `symbol`, or null while unresolvable. */
9455
+ canonicalSymbol(symbol) {
9456
+ return this.registry?.canonicalSymbol(symbol) ?? null;
9457
+ }
9399
9458
  /**
9400
9459
  * The registered provider INSTANCE under `name` — the seam for EXTENDED provider
9401
9460
  * surfaces: a provider may implement interfaces beyond the `DataProvider` port
@@ -9675,12 +9734,6 @@ function mix(base, top, alpha) {
9675
9734
  const c = (x, y) => Math.round(x + (y - x) * alpha);
9676
9735
  return `rgb(${c(b.r, t.r)},${c(b.g, t.g)},${c(b.b, t.b)})`;
9677
9736
  }
9678
- function toHex6(color, fallback = "#888888") {
9679
- const c = parseRgb(color);
9680
- if (!c) return fallback;
9681
- const h = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
9682
- return `#${h(c.r)}${h(c.g)}${h(c.b)}`;
9683
- }
9684
9737
  function isDarkColor(color) {
9685
9738
  const c = parseRgb(color);
9686
9739
  if (!c) return true;
@@ -9699,6 +9752,9 @@ var STATIC_TOKENS = {
9699
9752
  "--vela-z-tooltip": "60",
9700
9753
  "--vela-z-menu": "50",
9701
9754
  "--vela-z-dialog": "40",
9755
+ // Form popovers portal to <body> (or a chart host) and must sit above a dialog
9756
+ // whose own stacking context may be nested inside the chart container.
9757
+ "--vela-z-popover": "6000",
9702
9758
  "--vela-ease": "cubic-bezier(0.22, 1, 0.36, 1)",
9703
9759
  "--vela-dur-fast": "90ms",
9704
9760
  "--vela-dur-med": "160ms",
@@ -9881,6 +9937,1246 @@ function makeDialogDraggable(box, header, options) {
9881
9937
  return { reset };
9882
9938
  }
9883
9939
 
9940
+ // src/ui/styles.ts
9941
+ function injectStyles(id, css, root = document) {
9942
+ if (typeof document === "undefined") return;
9943
+ const host = root instanceof Document ? root.head : root;
9944
+ if (root.getElementById?.(id) ?? host.querySelector(`#${CSS.escape(id)}`)) return;
9945
+ const s = document.createElement("style");
9946
+ s.id = id;
9947
+ s.textContent = css;
9948
+ host.appendChild(s);
9949
+ }
9950
+
9951
+ // src/ui/icons.ts
9952
+ function iconEl(id, doc = document) {
9953
+ const span = doc.createElement("span");
9954
+ span.className = "vela-icon";
9955
+ span.setAttribute("aria-hidden", "true");
9956
+ const svg = iconMarkup(id);
9957
+ if (svg) span.innerHTML = svg;
9958
+ return span;
9959
+ }
9960
+
9961
+ // src/ui/components/switch/controller.ts
9962
+ function switchController(opts = {}) {
9963
+ let checked = opts.checked ?? false;
9964
+ const disabled = opts.disabled ?? false;
9965
+ return {
9966
+ get checked() {
9967
+ return checked;
9968
+ },
9969
+ disabled,
9970
+ size: opts.size ?? "md",
9971
+ tone: opts.tone ?? "bright",
9972
+ setChecked(v) {
9973
+ checked = v;
9974
+ },
9975
+ toggle() {
9976
+ if (disabled) return checked;
9977
+ checked = !checked;
9978
+ opts.onChange?.(checked);
9979
+ return checked;
9980
+ }
9981
+ };
9982
+ }
9983
+
9984
+ // src/ui/components/switch/styles.ts
9985
+ var SWITCH_STYLE_ID = "vela-ui-switch";
9986
+ var SWITCH_CSS = `
9987
+ .vela-switch {
9988
+ width: 20px;
9989
+ height: 20px;
9990
+ flex: 0 0 auto;
9991
+ display: inline-flex;
9992
+ align-items: center;
9993
+ justify-content: center;
9994
+ padding: 0;
9995
+ border: 1px solid var(--vela-border);
9996
+ border-radius: 5px;
9997
+ background: transparent;
9998
+ color: transparent;
9999
+ cursor: pointer;
10000
+ line-height: 0;
10001
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
10002
+ }
10003
+ .vela-switch .vela-icon, .vela-switch svg { display: block; width: 12px; height: 12px; }
10004
+ .vela-switch[data-size='sm'] .vela-icon, .vela-switch[data-size='sm'] svg { width: 11px; height: 11px; }
10005
+ .vela-switch:hover { border-color: var(--vela-fg-muted); }
10006
+ .vela-switch[data-checked] {
10007
+ background: var(--vela-fg-bright);
10008
+ border-color: var(--vela-fg-bright);
10009
+ color: var(--vela-bg);
10010
+ }
10011
+ .vela-switch[data-size='sm'] { width: 18px; height: 18px; border-color: var(--vela-border-strong); }
10012
+ .vela-switch[data-size='sm']:hover { border-color: var(--vela-fg-muted); }
10013
+ .vela-switch[data-size='sm'][data-checked] {
10014
+ background: var(--vela-selected-bg);
10015
+ border-color: var(--vela-selected-bg);
10016
+ color: var(--vela-selected-fg);
10017
+ }
10018
+ .vela-switch[data-tone='selected'][data-checked] {
10019
+ background: var(--vela-selected-bg);
10020
+ border-color: var(--vela-selected-bg);
10021
+ color: var(--vela-selected-fg);
10022
+ }
10023
+ .vela-switch[disabled] { opacity: 0.4; cursor: default; }
10024
+ `;
10025
+
10026
+ // src/ui/components/switch/view.ts
10027
+ var Switch = class {
10028
+ constructor(opts = {}) {
10029
+ injectStyles(SWITCH_STYLE_ID, SWITCH_CSS, document);
10030
+ this.ctrl = switchController(opts);
10031
+ const b = document.createElement("button");
10032
+ b.type = "button";
10033
+ if (opts.id) b.id = opts.id;
10034
+ b.className = "vela-switch";
10035
+ b.dataset.size = this.ctrl.size;
10036
+ b.dataset.tone = this.ctrl.tone;
10037
+ b.setAttribute("role", "switch");
10038
+ if (this.ctrl.disabled) b.disabled = true;
10039
+ b.appendChild(iconEl("check", b.ownerDocument));
10040
+ this.el = b;
10041
+ this.paint();
10042
+ b.addEventListener("pointerdown", (e) => {
10043
+ e.preventDefault();
10044
+ b.focus({ preventScroll: true });
10045
+ });
10046
+ b.addEventListener("click", () => {
10047
+ this.ctrl.toggle();
10048
+ this.paint();
10049
+ });
10050
+ }
10051
+ get checked() {
10052
+ return this.ctrl.checked;
10053
+ }
10054
+ setChecked(v) {
10055
+ this.ctrl.setChecked(v);
10056
+ this.paint();
10057
+ }
10058
+ paint() {
10059
+ const on = this.ctrl.checked;
10060
+ this.el.setAttribute("aria-checked", on ? "true" : "false");
10061
+ if (on) this.el.dataset.checked = "";
10062
+ else delete this.el.dataset.checked;
10063
+ }
10064
+ };
10065
+
10066
+ // src/ui/tokens.ts
10067
+ var STATIC_ID = "vela-ui-tokens";
10068
+ var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
10069
+ var STATIC_CSS = `
10070
+ .vela-ui, .vela-ui-layer {
10071
+ ${STATIC_DECLS}
10072
+ font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
10073
+ box-sizing: border-box;
10074
+ }
10075
+ .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
10076
+ .vela-icon { display: inline-flex; align-items: center; flex: none; }
10077
+ .vela-icon svg { display: block; }
10078
+ `;
10079
+ function applyThemeTokens(el, t) {
10080
+ const tokens = themeTokens(t);
10081
+ for (const key in tokens) el.style.setProperty(key, tokens[key]);
10082
+ }
10083
+ function applyPlotOverlayTokens(host, base, config) {
10084
+ const layout = config?.layout;
10085
+ const t = layout?.background && layout.textColor ? { ...base, background: layout.background, textColor: layout.textColor } : base;
10086
+ applyThemeTokens(host, t);
10087
+ }
10088
+ function ensureUIHost(el, theme) {
10089
+ injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
10090
+ el.classList.add("vela-ui");
10091
+ if (theme) applyThemeTokens(el, theme);
10092
+ }
10093
+
10094
+ // src/ui/components/popover/controller.ts
10095
+ function insetRect(r, inset) {
10096
+ return {
10097
+ left: r.left + inset,
10098
+ top: r.top + inset,
10099
+ right: r.right - inset,
10100
+ bottom: r.bottom - inset,
10101
+ width: r.width - inset * 2,
10102
+ height: r.height - inset * 2
10103
+ };
10104
+ }
10105
+ function viewportRect(width, height, inset) {
10106
+ return {
10107
+ left: inset,
10108
+ top: inset,
10109
+ right: width - inset,
10110
+ bottom: height - inset,
10111
+ width: width - inset * 2,
10112
+ height: height - inset * 2
10113
+ };
10114
+ }
10115
+ function intersectRects(a, b) {
10116
+ const left = Math.max(a.left, b.left);
10117
+ const top = Math.max(a.top, b.top);
10118
+ const right = Math.min(a.right, b.right);
10119
+ const bottom = Math.min(a.bottom, b.bottom);
10120
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
10121
+ }
10122
+ function placePopover(a) {
10123
+ let left = a.align === "end" ? a.trigger.right - a.pop.width : a.trigger.left;
10124
+ const below = a.trigger.bottom + a.gap;
10125
+ const above = a.trigger.top - a.pop.height - a.gap;
10126
+ const fitsBelow = below + a.pop.height <= a.clamp.bottom;
10127
+ const fitsAbove = above >= a.clamp.top;
10128
+ let top = below;
10129
+ if (!fitsBelow && (fitsAbove || a.trigger.top - a.clamp.top > a.clamp.bottom - a.trigger.bottom)) {
10130
+ top = Math.max(a.clamp.top, above);
10131
+ }
10132
+ if (left + a.pop.width > a.clamp.right) left = a.clamp.right - a.pop.width;
10133
+ if (left < a.clamp.left) left = a.clamp.left;
10134
+ if (top + a.pop.height > a.clamp.bottom) top = a.clamp.bottom - a.pop.height;
10135
+ if (top < a.clamp.top) top = a.clamp.top;
10136
+ return {
10137
+ left: Math.round(left - a.originX),
10138
+ top: Math.round(top - a.originY)
10139
+ };
10140
+ }
10141
+ function popoverController(opts = {}) {
10142
+ return {
10143
+ gap: opts.gap ?? 4,
10144
+ align: opts.align ?? "start",
10145
+ matchWidth: opts.matchWidth ?? false,
10146
+ position: opts.position ?? "fixed",
10147
+ boundaryInset: opts.boundaryInset ?? 0,
10148
+ viewportInset: opts.viewportInset ?? 6,
10149
+ onClose: opts.onClose
10150
+ };
10151
+ }
10152
+
10153
+ // src/ui/components/popover/styles.ts
10154
+ var POPOVER_STYLE_ID = "vela-ui-popover";
10155
+ var POPOVER_CSS = `
10156
+ .vela-popover {
10157
+ position: fixed;
10158
+ z-index: var(--vela-z-popover);
10159
+ box-sizing: border-box;
10160
+ /* Never serve as a scroll anchor: the shell is portaled and re-placed between
10161
+ gestures, and anchoring against it jumps the scroller underneath. */
10162
+ overflow-anchor: none;
10163
+ }
10164
+ .vela-popover[data-position='absolute'] { position: absolute; }
10165
+ .vela-popover[hidden] { display: none !important; }
10166
+ `;
10167
+
10168
+ // src/ui/components/popover/view.ts
10169
+ var open = null;
10170
+ function closeOpenPopovers() {
10171
+ open?.hide();
10172
+ }
10173
+ function isPopoverOpen() {
10174
+ return open !== null;
10175
+ }
10176
+ function openPopoverTrigger() {
10177
+ return open?.trigger ?? null;
10178
+ }
10179
+ function toRect(r) {
10180
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height };
10181
+ }
10182
+ var Popover = class {
10183
+ constructor(opts) {
10184
+ this.onOutside = null;
10185
+ this.onKey = null;
10186
+ this.onReflow = null;
10187
+ this.shown = false;
10188
+ const doc = opts.trigger.ownerDocument;
10189
+ injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
10190
+ this.trigger = opts.trigger;
10191
+ this.host = opts.host ?? doc.body;
10192
+ this.ctrl = popoverController(opts);
10193
+ this.boundary = opts.boundary ?? "viewport";
10194
+ this.theme = opts.theme;
10195
+ this.el = doc.createElement("div");
10196
+ this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
10197
+ this.el.dataset.position = this.ctrl.position;
10198
+ if (opts.zIndex !== void 0) this.el.style.zIndex = String(opts.zIndex);
10199
+ this.el.addEventListener("pointerdown", (e) => e.stopPropagation());
10200
+ if (opts.content instanceof Node) this.el.appendChild(opts.content);
10201
+ else if (typeof opts.content === "function") opts.content(this.el);
10202
+ }
10203
+ get open() {
10204
+ return this.shown;
10205
+ }
10206
+ get position() {
10207
+ return this.ctrl.position;
10208
+ }
10209
+ get align() {
10210
+ return this.ctrl.align;
10211
+ }
10212
+ show() {
10213
+ if (this.shown) {
10214
+ this.place();
10215
+ return;
10216
+ }
10217
+ if (open && open !== this) open.hide();
10218
+ ensureUIHost(this.el, this.theme);
10219
+ this.host.appendChild(this.el);
10220
+ this.shown = true;
10221
+ open = this;
10222
+ this.place();
10223
+ const onOutside = (ev) => {
10224
+ const t = ev.target;
10225
+ if (this.el.contains(t) || this.trigger.contains(t)) return;
10226
+ this.hide();
10227
+ };
10228
+ const onKey = (ev) => {
10229
+ if (ev.key !== "Escape") return;
10230
+ ev.preventDefault();
10231
+ ev.stopPropagation();
10232
+ this.hide();
10233
+ };
10234
+ const onReflow = () => this.place();
10235
+ setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
10236
+ document.addEventListener("keydown", onKey, true);
10237
+ window.addEventListener("resize", onReflow, true);
10238
+ document.addEventListener("scroll", onReflow, true);
10239
+ this.onOutside = onOutside;
10240
+ this.onKey = onKey;
10241
+ this.onReflow = onReflow;
10242
+ }
10243
+ hide() {
10244
+ if (!this.shown) return;
10245
+ if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
10246
+ if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
10247
+ if (this.onReflow) {
10248
+ window.removeEventListener("resize", this.onReflow, true);
10249
+ document.removeEventListener("scroll", this.onReflow, true);
10250
+ }
10251
+ this.onOutside = null;
10252
+ this.onKey = null;
10253
+ this.onReflow = null;
10254
+ this.el.remove();
10255
+ this.shown = false;
10256
+ if (open === this) open = null;
10257
+ this.ctrl.onClose?.();
10258
+ }
10259
+ /** Show if closed, hide if this instance is the open popover. */
10260
+ toggle() {
10261
+ if (this.shown) this.hide();
10262
+ else this.show();
10263
+ }
10264
+ destroy() {
10265
+ this.hide();
10266
+ }
10267
+ reposition() {
10268
+ if (this.shown) this.place();
10269
+ }
10270
+ clampRect() {
10271
+ const view = viewportRect(window.innerWidth, window.innerHeight, this.ctrl.viewportInset);
10272
+ const bound = this.readBoundary();
10273
+ if (!bound) return view;
10274
+ const inset = insetRect(bound, this.ctrl.boundaryInset);
10275
+ return intersectRects(view, inset);
10276
+ }
10277
+ readBoundary() {
10278
+ const b = this.boundary;
10279
+ if (b === "viewport") return null;
10280
+ const raw = typeof b === "function" ? b() : b.getBoundingClientRect();
10281
+ return raw ? toRect(raw) : null;
10282
+ }
10283
+ place() {
10284
+ const ar = this.trigger.getBoundingClientRect();
10285
+ if (this.ctrl.matchWidth) {
10286
+ this.el.style.minWidth = `${Math.round(Math.max(this.el.offsetWidth, ar.width))}px`;
10287
+ }
10288
+ const origin = this.ctrl.position === "absolute" ? this.host.getBoundingClientRect() : { left: 0, top: 0 };
10289
+ const pos = placePopover({
10290
+ trigger: toRect(ar),
10291
+ pop: { width: this.el.offsetWidth, height: this.el.offsetHeight },
10292
+ gap: this.ctrl.gap,
10293
+ align: this.ctrl.align,
10294
+ clamp: this.clampRect(),
10295
+ originX: origin.left,
10296
+ originY: origin.top
10297
+ });
10298
+ this.el.style.left = `${pos.left}px`;
10299
+ this.el.style.top = `${pos.top}px`;
10300
+ }
10301
+ };
10302
+
10303
+ // src/ui/components/select/controller.ts
10304
+ function selectController(opts) {
10305
+ const options = opts.options;
10306
+ let value = opts.value ?? options[0]?.value ?? "";
10307
+ const size = opts.size ?? "md";
10308
+ return {
10309
+ options,
10310
+ get value() {
10311
+ return value;
10312
+ },
10313
+ size,
10314
+ fill: opts.fill ?? size !== "sm",
10315
+ disabled: opts.disabled ?? false,
10316
+ setValue(v) {
10317
+ value = v;
10318
+ },
10319
+ labelOf(v) {
10320
+ return options.find((o) => o.value === v)?.label ?? v;
10321
+ },
10322
+ pick(v) {
10323
+ value = v;
10324
+ const label = options.find((o) => o.value === v)?.label ?? v;
10325
+ opts.onChange?.(v, label);
10326
+ return { value: v, label };
10327
+ }
10328
+ };
10329
+ }
10330
+
10331
+ // src/ui/components/select/styles.ts
10332
+ var SELECT_STYLE_ID = "vela-ui-select";
10333
+ var SELECT_CSS = `
10334
+ .vela-select { position: relative; display: inline-block; min-width: 0; }
10335
+ .vela-select[data-fill] { width: 100%; }
10336
+ .vela-select-trigger {
10337
+ display: flex;
10338
+ align-items: center;
10339
+ width: 100%;
10340
+ box-sizing: border-box;
10341
+ height: 34px;
10342
+ padding: 0 26px 0 8px;
10343
+ background: transparent;
10344
+ border: 1px solid var(--vela-border-strong);
10345
+ border-radius: 6px;
10346
+ color: var(--vela-fg-bright);
10347
+ font-size: 14px;
10348
+ font-family: inherit;
10349
+ cursor: pointer;
10350
+ text-align: left;
10351
+ outline: none;
10352
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
10353
+ }
10354
+ .vela-select-trigger:hover { border-color: var(--vela-fg-muted); }
10355
+ .vela-select-trigger:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
10356
+ /* As a grid item (chart-settings rows dissolve via display:contents into a shared
10357
+ max-content column) the wrap must NOT stretch to the column: it sizes to its own
10358
+ widest option, like the native select it replaces. */
10359
+ .vela-select[data-size='sm'] { max-width: 200px; flex: 0 0 auto; justify-self: start; }
10360
+ .vela-select[data-size='sm'] .vela-select-trigger {
10361
+ height: 28px;
10362
+ background: var(--vela-surface-elev);
10363
+ border-radius: var(--vela-radius-sm);
10364
+ color: var(--vela-fg);
10365
+ font-size: 13px;
10366
+ box-shadow: none;
10367
+ }
10368
+ .vela-select[data-size='sm'] .vela-select-trigger:focus { box-shadow: none; border-color: var(--vela-fg-muted); }
10369
+ .vela-select-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
10370
+ /* Invisible option-label stack: gives the shrink-to-fit wrap the WIDEST option's
10371
+ intrinsic width (like a native select), so picking a shorter/longer label never
10372
+ resizes the trigger and shifts the row around it. */
10373
+ .vela-select-sizer { visibility: hidden; height: 0; overflow: hidden; font-size: 14px; }
10374
+ .vela-select[data-size='sm'] .vela-select-sizer { font-size: 13px; }
10375
+ .vela-select-sizer span { display: block; height: 0; white-space: nowrap; padding: 0 26px 0 8px; border-inline: 1px solid transparent; }
10376
+ .vela-select-chevron {
10377
+ position: absolute;
10378
+ right: 8px;
10379
+ top: 50%;
10380
+ transform: translateY(-50%);
10381
+ pointer-events: none;
10382
+ display: flex;
10383
+ opacity: 0.55;
10384
+ line-height: 0;
10385
+ color: inherit;
10386
+ }
10387
+ .vela-select-chevron .vela-icon, .vela-select-chevron svg { width: 12px; height: 12px; display: block; }
10388
+ .vela-select-list {
10389
+ background: var(--vela-bg);
10390
+ color: var(--vela-fg);
10391
+ border: none;
10392
+ border-radius: 6px;
10393
+ box-shadow: var(--vela-shadow);
10394
+ font: 14px var(--vela-font);
10395
+ padding: 4px;
10396
+ overflow: hidden;
10397
+ }
10398
+ .vela-select-list[data-size='sm'] { font-size: 13px; background: var(--vela-surface-overlay); }
10399
+ .vela-select-items { max-height: none; overflow: hidden; }
10400
+ .vela-select-list.is-scroll { display: flex; align-items: stretch; gap: 2px; }
10401
+ .vela-select-list.is-scroll .vela-select-items {
10402
+ flex: 1 1 auto;
10403
+ min-width: 0;
10404
+ max-height: 240px;
10405
+ overflow-y: auto;
10406
+ scrollbar-width: none;
10407
+ }
10408
+ .vela-select-list.is-scroll .vela-select-items::-webkit-scrollbar { display: none; width: 0; height: 0; }
10409
+ .vela-select-rail { position: relative; flex: none; width: 3px; margin: 2px 1px 2px 0; pointer-events: none; }
10410
+ .vela-select-thumb { width: 3px; border-radius: 2px; background: var(--vela-scroll); }
10411
+ .vela-select-item {
10412
+ display: block;
10413
+ width: 100%;
10414
+ text-align: left;
10415
+ padding: 7px 10px;
10416
+ border: none;
10417
+ border-radius: 4px;
10418
+ background: transparent;
10419
+ color: inherit;
10420
+ cursor: pointer;
10421
+ font: inherit;
10422
+ font-weight: 400;
10423
+ }
10424
+ .vela-select-item:hover { background: var(--vela-hover); }
10425
+ .vela-select-item[data-checked] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
10426
+ .vela-select-item[data-checked]:hover { background: var(--vela-hover-strong); }
10427
+ `;
10428
+
10429
+ // src/ui/components/select/view.ts
10430
+ function ensureSelectStyles(doc) {
10431
+ injectStyles(SELECT_STYLE_ID, SELECT_CSS, doc);
10432
+ }
10433
+ function syncThumb(list, thumb) {
10434
+ const view = list.clientHeight;
10435
+ const total = list.scrollHeight;
10436
+ if (total <= view) {
10437
+ thumb.style.height = "0";
10438
+ return;
10439
+ }
10440
+ const thumbH = Math.max(20, view / total * view);
10441
+ const top = list.scrollTop / (total - view) * (view - thumbH);
10442
+ thumb.style.height = `${Math.round(thumbH)}px`;
10443
+ thumb.style.transform = `translateY(${Math.round(top)}px`;
10444
+ }
10445
+ function fillSelectList(menu2, options, current, onPick) {
10446
+ menu2.replaceChildren();
10447
+ const list = menu2.ownerDocument.createElement("div");
10448
+ list.className = "vela-select-items";
10449
+ for (const p of options) {
10450
+ const item = menu2.ownerDocument.createElement("button");
10451
+ item.type = "button";
10452
+ item.className = "vela-select-item";
10453
+ item.textContent = p.label;
10454
+ if (p.value === current) item.dataset.checked = "1";
10455
+ item.addEventListener("click", (e) => {
10456
+ e.stopPropagation();
10457
+ onPick(p.value, p.label);
10458
+ });
10459
+ list.appendChild(item);
10460
+ }
10461
+ menu2.appendChild(list);
10462
+ }
10463
+ function decorateSelectScroll(menu2) {
10464
+ const list = menu2.querySelector(".vela-select-items");
10465
+ if (!list) return;
10466
+ if (list.scrollHeight > 240) {
10467
+ menu2.classList.add("is-scroll");
10468
+ const rail = menu2.ownerDocument.createElement("div");
10469
+ rail.className = "vela-select-rail";
10470
+ const thumb = menu2.ownerDocument.createElement("div");
10471
+ thumb.className = "vela-select-thumb";
10472
+ rail.appendChild(thumb);
10473
+ menu2.appendChild(rail);
10474
+ list.addEventListener("scroll", () => syncThumb(list, thumb));
10475
+ syncThumb(list, thumb);
10476
+ }
10477
+ menu2.querySelector(".vela-select-item[data-checked]")?.scrollIntoView({ block: "nearest" });
10478
+ list.dispatchEvent(new Event("scroll"));
10479
+ }
10480
+ function toggleSelectList(trigger, options, current, onPick, opts = {}) {
10481
+ if (openPopoverTrigger() === trigger) {
10482
+ closeOpenPopovers();
10483
+ return null;
10484
+ }
10485
+ return openSelectList(trigger, options, current, onPick, opts);
10486
+ }
10487
+ function openSelectList(trigger, options, current, onPick, opts = {}) {
10488
+ ensureSelectStyles(trigger.ownerDocument);
10489
+ closeOpenPopovers();
10490
+ const pop = new Popover({
10491
+ trigger,
10492
+ theme: opts.theme,
10493
+ matchWidth: opts.matchWidth ?? true,
10494
+ boundary: opts.boundary,
10495
+ boundaryInset: opts.boundaryInset,
10496
+ gap: opts.gap ?? 4,
10497
+ align: opts.align ?? "start",
10498
+ host: opts.host,
10499
+ position: opts.position,
10500
+ zIndex: opts.zIndex,
10501
+ className: "vela-select-list",
10502
+ onClose: opts.onClose,
10503
+ content: (el) => {
10504
+ if (opts.size) el.dataset.size = opts.size;
10505
+ fillSelectList(el, options, current, (value, label) => {
10506
+ pop.hide();
10507
+ onPick(value, label);
10508
+ });
10509
+ }
10510
+ });
10511
+ pop.show();
10512
+ decorateSelectScroll(pop.el);
10513
+ pop.reposition();
10514
+ return pop;
10515
+ }
10516
+ var Select = class {
10517
+ constructor(opts) {
10518
+ this.list = null;
10519
+ const doc = opts.list?.host?.ownerDocument ?? document;
10520
+ ensureSelectStyles(doc);
10521
+ this.ctrl = selectController(opts);
10522
+ this.listOpts = { ...opts.list, theme: opts.theme ?? opts.list?.theme, size: this.ctrl.size };
10523
+ const wrap = doc.createElement("div");
10524
+ wrap.className = "vela-select";
10525
+ wrap.dataset.size = this.ctrl.size;
10526
+ if (this.ctrl.fill) wrap.dataset.fill = "";
10527
+ const btn = doc.createElement("button");
10528
+ btn.type = "button";
10529
+ if (opts.id) btn.id = opts.id;
10530
+ btn.className = "vela-select-trigger";
10531
+ if (this.ctrl.disabled) btn.disabled = true;
10532
+ const label = doc.createElement("span");
10533
+ label.className = "vela-select-label";
10534
+ label.textContent = this.ctrl.labelOf(this.ctrl.value);
10535
+ const chevron = doc.createElement("span");
10536
+ chevron.className = "vela-select-chevron";
10537
+ chevron.appendChild(iconEl("chevron-down", doc));
10538
+ btn.append(label, chevron);
10539
+ wrap.appendChild(btn);
10540
+ const sizer = doc.createElement("div");
10541
+ sizer.className = "vela-select-sizer";
10542
+ sizer.setAttribute("aria-hidden", "true");
10543
+ for (const o of this.ctrl.options) {
10544
+ const s = doc.createElement("span");
10545
+ s.textContent = o.label;
10546
+ sizer.appendChild(s);
10547
+ }
10548
+ wrap.appendChild(sizer);
10549
+ btn.addEventListener("pointerdown", (e) => {
10550
+ e.preventDefault();
10551
+ btn.focus({ preventScroll: true });
10552
+ });
10553
+ btn.addEventListener("click", (e) => {
10554
+ e.stopPropagation();
10555
+ this.toggle();
10556
+ });
10557
+ this.el = wrap;
10558
+ this.trigger = btn;
10559
+ this.labelEl = label;
10560
+ }
10561
+ get value() {
10562
+ return this.ctrl.value;
10563
+ }
10564
+ setValue(v) {
10565
+ this.ctrl.setValue(v);
10566
+ this.labelEl.textContent = this.ctrl.labelOf(v);
10567
+ }
10568
+ toggle() {
10569
+ this.list = toggleSelectList(
10570
+ this.trigger,
10571
+ this.ctrl.options,
10572
+ this.ctrl.value,
10573
+ (value, label) => {
10574
+ this.ctrl.pick(value);
10575
+ this.labelEl.textContent = label;
10576
+ this.list = null;
10577
+ },
10578
+ { ...this.listOpts, onClose: () => {
10579
+ this.list = null;
10580
+ this.listOpts.onClose?.();
10581
+ } }
10582
+ );
10583
+ }
10584
+ destroy() {
10585
+ this.list?.hide();
10586
+ this.list = null;
10587
+ }
10588
+ };
10589
+
10590
+ // src/ui/components/number-input/controller.ts
10591
+ function clampNumber(n, opts) {
10592
+ let v = n;
10593
+ if (opts.min !== void 0) v = Math.max(opts.min, v);
10594
+ if (opts.max !== void 0) v = Math.min(opts.max, v);
10595
+ if (opts.integer) v = Math.round(v);
10596
+ return v;
10597
+ }
10598
+ function decimalsOf(n) {
10599
+ const s = String(n);
10600
+ const e = s.search(/[eE]/);
10601
+ if (e >= 0) {
10602
+ const frac = s.slice(0, e).split(".")[1]?.length ?? 0;
10603
+ return Math.max(0, Math.min(20, frac - Number(s.slice(e + 1))));
10604
+ }
10605
+ return s.split(".")[1]?.length ?? 0;
10606
+ }
10607
+ function snapToStep(n, base, step) {
10608
+ const d = Math.min(20, Math.max(decimalsOf(base), decimalsOf(step)));
10609
+ return Number(n.toFixed(d));
10610
+ }
10611
+ function numberInputController(opts) {
10612
+ let value = opts.value;
10613
+ const integer = opts.integer ?? false;
10614
+ const commit = opts.commit ?? "blur";
10615
+ const clamp = opts.clamp ?? commit === "blur";
10616
+ const step = opts.step ?? (integer ? 1 : 0.1);
10617
+ const onChange = opts.onChange;
10618
+ const normalize = (raw) => clamp ? clampNumber(raw, { min: opts.min, max: opts.max, integer }) : raw;
10619
+ const apply = (raw) => {
10620
+ if (!Number.isFinite(raw)) return null;
10621
+ const n = normalize(raw);
10622
+ if (n !== value) {
10623
+ value = n;
10624
+ onChange?.(n);
10625
+ }
10626
+ return n;
10627
+ };
10628
+ return {
10629
+ get value() {
10630
+ return value;
10631
+ },
10632
+ min: opts.min,
10633
+ max: opts.max,
10634
+ step,
10635
+ integer,
10636
+ size: opts.size ?? "md",
10637
+ commit,
10638
+ steppers: opts.steppers ?? commit === "blur",
10639
+ clamp,
10640
+ disabled: opts.disabled ?? false,
10641
+ apply,
10642
+ sync(raw) {
10643
+ const n = Number.isFinite(raw) ? normalize(raw) : value;
10644
+ value = n;
10645
+ return n;
10646
+ },
10647
+ nudge(dir) {
10648
+ return apply(snapToStep(value + dir * step, value, step));
10649
+ }
10650
+ };
10651
+ }
10652
+
10653
+ // src/ui/components/number-input/styles.ts
10654
+ var NUMBER_STYLE_ID = "vela-ui-number";
10655
+ var NUMBER_CSS = `
10656
+ .vela-num { position: relative; display: inline-block; min-width: 0; }
10657
+ .vela-num[data-fill] { width: 100%; }
10658
+ .vela-num input {
10659
+ width: 100%;
10660
+ box-sizing: border-box;
10661
+ height: 34px;
10662
+ background: transparent;
10663
+ border: 1px solid var(--vela-border-strong);
10664
+ border-radius: 6px;
10665
+ color: var(--vela-fg-bright);
10666
+ padding: 0 8px;
10667
+ font-size: 14px;
10668
+ font-family: inherit;
10669
+ outline: none;
10670
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
10671
+ -moz-appearance: textfield;
10672
+ appearance: textfield;
10673
+ }
10674
+ .vela-num input::-webkit-inner-spin-button, .vela-num input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
10675
+ .vela-num input:hover { border-color: var(--vela-fg-muted); }
10676
+ .vela-num input:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
10677
+ .vela-num[data-steppers] input { padding-right: 26px; }
10678
+ .vela-num[data-size='sm'] { width: 64px; flex: none; }
10679
+ .vela-num[data-size='sm'][data-compact] { width: 56px; }
10680
+ .vela-num[data-size='sm'] input {
10681
+ height: 28px;
10682
+ background: var(--vela-surface-elev);
10683
+ border-radius: var(--vela-radius-sm);
10684
+ color: var(--vela-fg);
10685
+ font-size: 13px;
10686
+ box-shadow: none;
10687
+ }
10688
+ .vela-num[data-size='sm'] input:focus { box-shadow: none; }
10689
+ .vela-num-step {
10690
+ position: absolute;
10691
+ right: 0;
10692
+ top: 0;
10693
+ bottom: 0;
10694
+ width: 22px;
10695
+ display: none;
10696
+ flex-direction: column;
10697
+ justify-content: center;
10698
+ box-sizing: border-box;
10699
+ padding: 2px 6px 2px 0;
10700
+ line-height: 0;
10701
+ }
10702
+ .vela-num[data-steppers]:hover .vela-num-step { display: flex; }
10703
+ .vela-num-step button {
10704
+ flex: 1;
10705
+ width: 100%;
10706
+ min-height: 0;
10707
+ border: none;
10708
+ background: transparent;
10709
+ color: var(--vela-fg-muted);
10710
+ border-radius: 3px;
10711
+ padding: 0;
10712
+ cursor: pointer;
10713
+ display: flex;
10714
+ align-items: center;
10715
+ justify-content: center;
10716
+ }
10717
+ .vela-num-step button:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
10718
+ .vela-num-step .vela-icon, .vela-num-step svg { width: 12px; height: 12px; display: block; }
10719
+ `;
10720
+
10721
+ // src/ui/components/number-input/view.ts
10722
+ var NumberInput = class {
10723
+ constructor(opts) {
10724
+ const doc = document;
10725
+ injectStyles(NUMBER_STYLE_ID, NUMBER_CSS, doc);
10726
+ this.ctrl = numberInputController(opts);
10727
+ this.placeholderMode = opts.placeholder !== void 0;
10728
+ this.emptyValue = opts.emptyValue ?? opts.value;
10729
+ this.last = String(opts.value);
10730
+ const wrap = doc.createElement("div");
10731
+ wrap.className = "vela-num";
10732
+ wrap.dataset.size = this.ctrl.size;
10733
+ if (this.ctrl.size !== "sm") wrap.dataset.fill = "";
10734
+ if (opts.compact) wrap.dataset.compact = "";
10735
+ if (this.ctrl.steppers) wrap.dataset.steppers = "";
10736
+ const ni = doc.createElement("input");
10737
+ ni.type = "number";
10738
+ if (opts.id) ni.id = opts.id;
10739
+ if (opts.title) ni.title = opts.title;
10740
+ if (opts.min !== void 0) ni.min = String(opts.min);
10741
+ if (opts.max !== void 0) ni.max = String(opts.max);
10742
+ ni.step = String(this.ctrl.step);
10743
+ if (this.ctrl.disabled) ni.disabled = true;
10744
+ if (this.placeholderMode) {
10745
+ ni.placeholder = opts.placeholder ?? "";
10746
+ ni.value = opts.value !== this.emptyValue ? String(opts.value) : "";
10747
+ } else {
10748
+ ni.value = String(opts.value);
10749
+ }
10750
+ const commitRaw = (raw) => {
10751
+ const n = this.ctrl.apply(raw);
10752
+ if (n === null) {
10753
+ ni.value = this.placeholderMode && this.ctrl.value === this.emptyValue ? "" : this.last;
10754
+ return;
10755
+ }
10756
+ this.last = String(n);
10757
+ if (this.placeholderMode && n === this.emptyValue) ni.value = "";
10758
+ else ni.value = String(n);
10759
+ };
10760
+ if (this.placeholderMode) {
10761
+ ni.addEventListener("change", () => {
10762
+ const raw = ni.value.trim() === "" ? this.emptyValue : Number(ni.value);
10763
+ commitRaw(Number.isFinite(raw) ? raw : this.emptyValue);
10764
+ });
10765
+ } else if (this.ctrl.commit === "live") {
10766
+ ni.addEventListener("input", () => {
10767
+ const v = Number(ni.value);
10768
+ if (Number.isFinite(v)) this.ctrl.apply(v);
10769
+ });
10770
+ } else {
10771
+ ni.addEventListener("blur", () => commitRaw(Number(ni.value)));
10772
+ ni.addEventListener("keydown", (e) => {
10773
+ if (e.key === "Enter") {
10774
+ e.preventDefault();
10775
+ ni.blur();
10776
+ }
10777
+ });
10778
+ }
10779
+ wrap.appendChild(ni);
10780
+ if (this.ctrl.steppers) {
10781
+ wrap.appendChild(this.buildSteppers(doc, (dir) => {
10782
+ const cur = Number(ni.value);
10783
+ const base = Number.isFinite(cur) ? cur : this.ctrl.value;
10784
+ commitRaw(snapToStep(base + dir * this.ctrl.step, base, this.ctrl.step));
10785
+ }));
10786
+ }
10787
+ this.el = wrap;
10788
+ this.input = ni;
10789
+ }
10790
+ get value() {
10791
+ return this.ctrl.value;
10792
+ }
10793
+ setValue(v) {
10794
+ const n = this.ctrl.sync(v);
10795
+ this.last = String(n);
10796
+ if (this.placeholderMode && n === this.emptyValue) this.input.value = "";
10797
+ else this.input.value = String(n);
10798
+ }
10799
+ buildSteppers(doc, applyDir) {
10800
+ const steps = doc.createElement("div");
10801
+ steps.className = "vela-num-step";
10802
+ const mk = (dir, icon2, label) => {
10803
+ const b = doc.createElement("button");
10804
+ b.type = "button";
10805
+ b.tabIndex = -1;
10806
+ b.setAttribute("aria-label", label);
10807
+ b.appendChild(iconEl(icon2, doc));
10808
+ let timer = 0;
10809
+ const apply = () => applyDir(dir);
10810
+ const stop = () => {
10811
+ if (timer) {
10812
+ window.clearTimeout(timer);
10813
+ timer = 0;
10814
+ }
10815
+ };
10816
+ b.addEventListener("pointerdown", (e) => {
10817
+ e.preventDefault();
10818
+ e.stopPropagation();
10819
+ apply();
10820
+ timer = window.setTimeout(function tick() {
10821
+ apply();
10822
+ timer = window.setTimeout(tick, 60);
10823
+ }, 400);
10824
+ });
10825
+ b.addEventListener("pointerup", stop);
10826
+ b.addEventListener("pointerleave", stop);
10827
+ b.addEventListener("pointercancel", stop);
10828
+ return b;
10829
+ };
10830
+ steps.append(mk(1, "chevron-up", "Increase"), mk(-1, "chevron-down", "Decrease"));
10831
+ return steps;
10832
+ }
10833
+ };
10834
+
10835
+ // src/ui/components/text-field/controller.ts
10836
+ function textFieldController(opts = {}) {
10837
+ let value = opts.value ?? "";
10838
+ const size = opts.size ?? "md";
10839
+ return {
10840
+ get value() {
10841
+ return value;
10842
+ },
10843
+ size,
10844
+ fill: opts.fill ?? size !== "sm",
10845
+ disabled: opts.disabled ?? false,
10846
+ commit(next) {
10847
+ if (next === value) return null;
10848
+ value = next;
10849
+ opts.onChange?.(next);
10850
+ return next;
10851
+ },
10852
+ sync(next) {
10853
+ value = next;
10854
+ }
10855
+ };
10856
+ }
10857
+
10858
+ // src/ui/components/text-field/styles.ts
10859
+ var TEXT_STYLE_ID = "vela-ui-text";
10860
+ var TEXT_CSS = `
10861
+ .vela-text {
10862
+ display: inline-block;
10863
+ min-width: 0;
10864
+ }
10865
+ .vela-text[data-fill] { width: 100%; }
10866
+ .vela-text-field {
10867
+ width: 100%;
10868
+ box-sizing: border-box;
10869
+ height: 34px;
10870
+ background: transparent;
10871
+ border: 1px solid var(--vela-border-strong);
10872
+ border-radius: 6px;
10873
+ color: var(--vela-fg-bright);
10874
+ padding: 0 8px;
10875
+ font-size: 14px;
10876
+ font-family: inherit;
10877
+ outline: none;
10878
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
10879
+ }
10880
+ .vela-text-field:hover { border-color: var(--vela-fg-muted); }
10881
+ .vela-text-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
10882
+ .vela-text[data-size='sm'] .vela-text-field {
10883
+ height: 28px;
10884
+ background: var(--vela-surface-elev);
10885
+ border-radius: var(--vela-radius-sm);
10886
+ color: var(--vela-fg);
10887
+ font-size: 13px;
10888
+ }
10889
+ `;
10890
+
10891
+ // src/ui/components/text-field/view.ts
10892
+ var TextField = class {
10893
+ constructor(opts = {}) {
10894
+ const doc = document;
10895
+ injectStyles(TEXT_STYLE_ID, TEXT_CSS, doc);
10896
+ this.ctrl = textFieldController(opts);
10897
+ const wrap = doc.createElement("div");
10898
+ wrap.className = "vela-text";
10899
+ wrap.dataset.size = this.ctrl.size;
10900
+ if (this.ctrl.fill) wrap.dataset.fill = "";
10901
+ const ti = doc.createElement("input");
10902
+ ti.type = "text";
10903
+ if (opts.id) ti.id = opts.id;
10904
+ ti.value = this.ctrl.value;
10905
+ ti.className = "vela-text-field";
10906
+ if (this.ctrl.disabled) ti.disabled = true;
10907
+ ti.addEventListener("blur", () => {
10908
+ this.ctrl.commit(ti.value);
10909
+ });
10910
+ ti.addEventListener("keydown", (e) => {
10911
+ if (e.key === "Enter") {
10912
+ e.preventDefault();
10913
+ ti.blur();
10914
+ }
10915
+ });
10916
+ wrap.appendChild(ti);
10917
+ this.el = wrap;
10918
+ this.input = ti;
10919
+ }
10920
+ get value() {
10921
+ return this.ctrl.value;
10922
+ }
10923
+ setValue(v) {
10924
+ this.ctrl.sync(v);
10925
+ this.input.value = v;
10926
+ }
10927
+ };
10928
+
10929
+ // src/ui/components/color-picker/controller.ts
10930
+ function splitColor(color) {
10931
+ const s = String(color ?? "").trim();
10932
+ let m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
10933
+ if (m) return { hex6: `#${m[1].toLowerCase()}`, alpha: m[2] ? parseInt(m[2], 16) / 255 : 1 };
10934
+ m = /^#([0-9a-f]{3})$/i.exec(s);
10935
+ if (m) {
10936
+ const h = m[1].toLowerCase();
10937
+ return { hex6: `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`, alpha: 1 };
10938
+ }
10939
+ const rgba = /^rgba?\(([^)]+)\)/i.exec(s);
10940
+ if (rgba) {
10941
+ const p = rgba[1].split(",").map((v) => v.trim());
10942
+ const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
10943
+ const a = p[3] != null ? Math.max(0, Math.min(1, parseFloat(p[3]))) : 1;
10944
+ return { hex6: `#${hx(parseInt(p[0] ?? "0", 10))}${hx(parseInt(p[1] ?? "0", 10))}${hx(parseInt(p[2] ?? "0", 10))}`, alpha: a };
10945
+ }
10946
+ return { hex6: ACCENT_BRIGHT, alpha: 1 };
10947
+ }
10948
+ function combineColor(hex6, alpha) {
10949
+ const a = Math.max(0, Math.min(1, alpha));
10950
+ if (a >= 0.999) return hex6;
10951
+ return `${hex6}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
10952
+ }
10953
+ function blendOver(fg, bg, alpha) {
10954
+ const rgb = (c) => {
10955
+ const n = parseInt(splitColor(c).hex6.slice(1), 16);
10956
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
10957
+ };
10958
+ const [fr, fgc, fb] = rgb(fg);
10959
+ const [br, bgc, bb] = rgb(bg);
10960
+ const a = Math.max(0, Math.min(1, alpha));
10961
+ const h = (n) => Math.round(n).toString(16).padStart(2, "0");
10962
+ return `#${h(fr * a + br * (1 - a))}${h(fgc * a + bgc * (1 - a))}${h(fb * a + bb * (1 - a))}`;
10963
+ }
10964
+ function hslHex(h, s, l) {
10965
+ const sn = s / 100;
10966
+ const ln = l / 100;
10967
+ const k = (n) => (n + h / 30) % 12;
10968
+ const a = sn * Math.min(ln, 1 - ln);
10969
+ const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
10970
+ const to = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
10971
+ return `#${to(f(0))}${to(f(8))}${to(f(4))}`;
10972
+ }
10973
+ function buildPalette() {
10974
+ const grays = [100, 90, 80, 68, 56, 45, 35, 25, 14, 0].map((l) => hslHex(0, 0, l));
10975
+ const hues = [4, 28, 50, 96, 140, 174, 200, 224, 270, 322];
10976
+ const levels = [
10977
+ [82, 56],
10978
+ [58, 84],
10979
+ [62, 74],
10980
+ [66, 62],
10981
+ [70, 50],
10982
+ [64, 39],
10983
+ [54, 29]
10984
+ ];
10985
+ return [grays, ...levels.map(([s, l]) => hues.map((h) => hslHex(h, s, l)))];
10986
+ }
10987
+ function transparencyChecker(size) {
10988
+ return `repeating-conic-gradient(#9aa0a6 0% 25%, #d3d6da 0% 50%) 0 0 / ${size}px ${size}px`;
10989
+ }
10990
+
10991
+ // src/ui/components/color-picker/styles.ts
10992
+ var COLOR_STYLE_ID = "vela-ui-color";
10993
+ var COLOR_CSS = `
10994
+ .vela-color-field{width:24px;height:24px;padding:2px;border:1px solid var(--vela-border);border-radius:0;background:var(--vela-surface-sunken);cursor:pointer;display:inline-flex;flex:none;}
10995
+ .vela-color-field:hover{border-color:var(--vela-fg-muted);}
10996
+ .vela-color-field-swatch{display:block;width:100%;height:100%;border-radius:0;box-shadow:inset 0 0 0 1px rgba(0,0,0,0.25);}
10997
+ .vela-color-field-circle{width:26px;height:26px;padding:3px;border:1px solid var(--vela-border-strong);border-radius:4px;background:transparent;overflow:hidden;}
10998
+ .vela-color-field-circle .vela-color-field-swatch{border-radius:2px;box-shadow:none;}
10999
+ .vela-color-field-pop{background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:10px;}
11000
+ `;
11001
+
11002
+ // src/ui/components/color-picker/view.ts
11003
+ var PALETTE = buildPalette();
11004
+ var CHECKER = transparencyChecker(10);
11005
+ var FIELD_CHECKER = transparencyChecker(8);
11006
+ var recents = [ACCENT, BULLISH, BEARISH, WARNING, NEUTRAL];
11007
+ function addRecent(hex6) {
11008
+ const i = recents.findIndex((c) => c.toLowerCase() === hex6.toLowerCase());
11009
+ if (i >= 0) recents.splice(i, 1);
11010
+ recents.unshift(hex6);
11011
+ if (recents.length > 10) recents.length = 10;
11012
+ }
11013
+ function buildColorPicker(color, theme, onChange) {
11014
+ const parsed = splitColor(color);
11015
+ let curHex = parsed.hex6;
11016
+ let curAlpha = parsed.alpha;
11017
+ const root = document.createElement("div");
11018
+ root.style.cssText = "display:flex;flex-direction:column;gap:9px;width:236px;";
11019
+ const grid = document.createElement("div");
11020
+ grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
11021
+ const swatches = [];
11022
+ for (const row of PALETTE) {
11023
+ for (const c of row) {
11024
+ const sw = document.createElement("button");
11025
+ sw.type = "button";
11026
+ sw.dataset.c = c;
11027
+ sw.style.cssText = `width:100%;aspect-ratio:1;border-radius:4px;border:none;cursor:pointer;background:${c};padding:0;`;
11028
+ sw.addEventListener("click", (e) => {
11029
+ e.stopPropagation();
11030
+ pickHex(c);
11031
+ });
11032
+ grid.appendChild(sw);
11033
+ swatches.push(sw);
11034
+ }
11035
+ }
11036
+ const paintSelection = () => {
11037
+ for (const sw of swatches) {
11038
+ const on = (sw.dataset.c ?? "").toLowerCase() === curHex.toLowerCase();
11039
+ sw.style.outline = on ? `2px solid ${theme.textColor}` : "none";
11040
+ sw.style.outlineOffset = "2px";
11041
+ sw.style.zIndex = on ? "1" : "";
11042
+ sw.style.position = on ? "relative" : "";
11043
+ }
11044
+ };
11045
+ const recentRow = document.createElement("div");
11046
+ recentRow.style.cssText = "display:flex;align-items:center;gap:5px;flex-wrap:wrap;border-top:1px solid var(--vela-border);padding-top:9px;";
11047
+ const renderRecents = () => {
11048
+ recentRow.replaceChildren();
11049
+ for (const c of recents) {
11050
+ const sw = document.createElement("button");
11051
+ sw.type = "button";
11052
+ sw.style.cssText = `width:17px;height:17px;border-radius:var(--vela-radius-sm);border:1px solid var(--vela-border);cursor:pointer;background:${c};padding:0;`;
11053
+ sw.addEventListener("click", (e) => {
11054
+ e.stopPropagation();
11055
+ pickHex(c);
11056
+ });
11057
+ recentRow.appendChild(sw);
11058
+ }
11059
+ const add = document.createElement("label");
11060
+ add.style.cssText = `width:17px;height:17px;border-radius:var(--vela-radius-sm);border:1px dashed var(--vela-border-strong);cursor:pointer;display:flex;align-items:center;justify-content:center;color:${theme.textColor};font:14px ${theme.fontFamily};position:relative;`;
11061
+ add.textContent = "+";
11062
+ const input = document.createElement("input");
11063
+ input.type = "color";
11064
+ input.value = curHex;
11065
+ input.style.cssText = "position:absolute;inset:0;opacity:0;cursor:pointer;";
11066
+ input.addEventListener("input", () => pickHex(input.value, true));
11067
+ add.appendChild(input);
11068
+ recentRow.appendChild(add);
11069
+ };
11070
+ const opLabel = document.createElement("div");
11071
+ opLabel.textContent = "Opacity";
11072
+ opLabel.style.cssText = `font:11px ${theme.fontFamily};color:var(--vela-fg-muted);`;
11073
+ const opRow = document.createElement("div");
11074
+ opRow.style.cssText = "display:flex;align-items:center;gap:10px;";
11075
+ const track = document.createElement("div");
11076
+ track.style.cssText = "flex:1;position:relative;height:13px;border-radius:7px;cursor:pointer;";
11077
+ const knob = document.createElement("div");
11078
+ knob.style.cssText = "position:absolute;top:50%;width:15px;height:15px;border-radius:50%;background:var(--vela-selected-bg);box-shadow:0 1px 3px rgba(0,0,0,0.55);transform:translate(-50%,-50%);pointer-events:none;";
11079
+ track.appendChild(knob);
11080
+ const pctBox = document.createElement("div");
11081
+ pctBox.style.cssText = `min-width:42px;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;`;
11082
+ opRow.append(track, pctBox);
11083
+ const paintOpacity = () => {
11084
+ track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
11085
+ knob.style.left = `${curAlpha * 100}%`;
11086
+ pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
11087
+ };
11088
+ let dragging = false;
11089
+ const onDrag = (clientX) => {
11090
+ const r = track.getBoundingClientRect();
11091
+ curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
11092
+ paintOpacity();
11093
+ emit();
11094
+ };
11095
+ track.addEventListener("pointerdown", (e) => {
11096
+ e.stopPropagation();
11097
+ dragging = true;
11098
+ track.setPointerCapture(e.pointerId);
11099
+ onDrag(e.clientX);
11100
+ });
11101
+ track.addEventListener("pointermove", (e) => {
11102
+ if (dragging) onDrag(e.clientX);
11103
+ });
11104
+ track.addEventListener("pointerup", () => dragging = false);
11105
+ function emit() {
11106
+ onChange(combineColor(curHex, curAlpha));
11107
+ }
11108
+ function pickHex(hex, custom = false) {
11109
+ curHex = splitColor(hex).hex6;
11110
+ addRecent(curHex);
11111
+ paintSelection();
11112
+ paintOpacity();
11113
+ if (custom) renderRecents();
11114
+ emit();
11115
+ }
11116
+ renderRecents();
11117
+ paintSelection();
11118
+ paintOpacity();
11119
+ root.append(grid, recentRow, opLabel, opRow);
11120
+ return root;
11121
+ }
11122
+ function colorField(theme, getVal, onVal, opts) {
11123
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
11124
+ }
11125
+ var ColorField = class {
11126
+ constructor(opts) {
11127
+ injectStyles(COLOR_STYLE_ID, COLOR_CSS, document);
11128
+ this.theme = opts.theme;
11129
+ this.getVal = opts.getVal;
11130
+ this.onVal = opts.onVal;
11131
+ this.popoverOpts = opts.popover;
11132
+ const trigger = document.createElement("button");
11133
+ trigger.type = "button";
11134
+ if (opts.id) trigger.id = opts.id;
11135
+ trigger.className = opts.shape === "circle" ? "vela-color-field vela-color-field-circle" : "vela-color-field";
11136
+ const swatch = document.createElement("span");
11137
+ swatch.className = "vela-color-field-swatch";
11138
+ trigger.appendChild(swatch);
11139
+ this.el = trigger;
11140
+ this.swatch = swatch;
11141
+ this.paint();
11142
+ trigger.addEventListener("vela-sync", () => this.paint());
11143
+ trigger.addEventListener("pointerdown", (e) => {
11144
+ e.preventDefault();
11145
+ trigger.focus({ preventScroll: true });
11146
+ });
11147
+ trigger.addEventListener("click", (e) => {
11148
+ e.stopPropagation();
11149
+ this.toggle();
11150
+ });
11151
+ }
11152
+ paint() {
11153
+ const v = this.getVal();
11154
+ this.swatch.style.background = `linear-gradient(${v}, ${v}), ${FIELD_CHECKER}`;
11155
+ }
11156
+ toggle() {
11157
+ if (openPopoverTrigger() === this.el) {
11158
+ closeOpenPopovers();
11159
+ return;
11160
+ }
11161
+ const pop = new Popover({
11162
+ trigger: this.el,
11163
+ theme: this.theme,
11164
+ align: this.popoverOpts?.align ?? "end",
11165
+ gap: this.popoverOpts?.gap ?? 6,
11166
+ host: this.popoverOpts?.host,
11167
+ position: this.popoverOpts?.position,
11168
+ boundary: this.popoverOpts?.boundary,
11169
+ zIndex: this.popoverOpts?.zIndex,
11170
+ className: "vela-color-field-pop",
11171
+ content: buildColorPicker(this.getVal(), this.theme, (val) => {
11172
+ this.onVal(val);
11173
+ this.paint();
11174
+ })
11175
+ });
11176
+ pop.show();
11177
+ }
11178
+ };
11179
+
9884
11180
  // src/renderers/shared/InputsUI.ts
9885
11181
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
9886
11182
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
@@ -9889,6 +11185,7 @@ var LEGEND_CTL_PX = 18;
9889
11185
  var LEGEND_ROW_PAD_Y = 2;
9890
11186
  var LEGEND_ROW_PAD_X = 6;
9891
11187
  var LEGEND_TITLE_VALUES_GAP_PX = 6;
11188
+ var LEGEND_TITLE_STATUS_GAP_PX = 8;
9892
11189
  var LEGEND_ACTION_GAP_PX = 6;
9893
11190
  var LEGEND_CTL_CSS = `cursor:pointer;display:inline-flex;align-items:center;justify-content:center;border:none;line-height:0;padding:0;width:${LEGEND_CTL_PX}px;height:${LEGEND_CTL_PX}px;border-radius:3px;box-sizing:border-box;flex:none;`;
9894
11191
  var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
@@ -9933,6 +11230,11 @@ var InputsUI = class {
9933
11230
  this.dialogTips = [];
9934
11231
  /** Open "Move to" menu (kept so it can be torn down). */
9935
11232
  this.moveMenu = null;
11233
+ /** Open settings-dialog choice menu (custom dropdown — torn down with the dialog). */
11234
+ this.choicePop = null;
11235
+ /** Open settings-dialog calendar popover (torn down with the dialog). */
11236
+ this.calendarPop = null;
11237
+ this.calendarAnchor = null;
9936
11238
  /** Collapsed panes → the master indicator id to keep visible (others hidden in the strip). */
9937
11239
  this.paneCollapse = /* @__PURE__ */ new Map();
9938
11240
  /** The user folded the indicator legend away behind the chevron — CHART-WIDE: every
@@ -9958,6 +11260,10 @@ var InputsUI = class {
9958
11260
  this.onDialogKey = (e) => {
9959
11261
  if (e.key === "Escape") {
9960
11262
  e.preventDefault();
11263
+ if (isPopoverOpen()) {
11264
+ closeOpenPopovers();
11265
+ return;
11266
+ }
9961
11267
  this.closeDialog();
9962
11268
  }
9963
11269
  };
@@ -10403,6 +11709,7 @@ var InputsUI = class {
10403
11709
  titleWrap.appendChild(beta);
10404
11710
  }
10405
11711
  el.appendChild(titleWrap);
11712
+ el.appendChild(statusEl);
10406
11713
  const valuesEl = document.createElement("span");
10407
11714
  valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
10408
11715
  el.appendChild(valuesEl);
@@ -10463,7 +11770,6 @@ var InputsUI = class {
10463
11770
  close.addEventListener("click", () => this.onRemove?.(id));
10464
11771
  controlsEl.appendChild(close);
10465
11772
  el.appendChild(controlsEl);
10466
- el.appendChild(statusEl);
10467
11773
  this.attach(this.legendFor(paneId), el, !!opts.native);
10468
11774
  this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
10469
11775
  this.syncFoldToggle();
@@ -10481,7 +11787,7 @@ var InputsUI = class {
10481
11787
  /**
10482
11788
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
10483
11789
  * dots (a fetch is in flight — the same load affordance as the chart's own bar-load dots,
10484
- * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered at the row's right end.
11790
+ * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered just right of the title.
10485
11791
  */
10486
11792
  setStatus(id, status) {
10487
11793
  const row = this.rows.get(id);
@@ -10493,7 +11799,7 @@ var InputsUI = class {
10493
11799
  return;
10494
11800
  }
10495
11801
  if (status === "loading") {
10496
- el.style.cssText = "display:inline-flex;align-items:center;gap:3px;box-sizing:border-box;flex:none;";
11802
+ el.style.cssText = `display:inline-flex;align-items:center;gap:3px;box-sizing:border-box;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;line-height:0;transform:translateY(1px);`;
10497
11803
  for (let i = 0; i < 3; i += 1) {
10498
11804
  const dot = document.createElement("span");
10499
11805
  dot.style.cssText = "width:4px;height:4px;border-radius:50%;background:currentColor;opacity:0.15;flex:none;";
@@ -10503,7 +11809,7 @@ var InputsUI = class {
10503
11809
  return;
10504
11810
  }
10505
11811
  this.ensureStatusKeyframes();
10506
- el.style.cssText = `display:inline-block;box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;background:${this.theme.upColor};animation:vela-ind-pulse 1.2s ease-in-out infinite;`;
11812
+ el.style.cssText = `display:inline-block;box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;transform:translateY(1px);background:${this.theme.upColor};animation:vela-ind-pulse 1.2s ease-in-out infinite;`;
10507
11813
  }
10508
11814
  /** Inject the status keyframes once (idempotent). */
10509
11815
  ensureStatusKeyframes() {
@@ -10556,15 +11862,15 @@ var InputsUI = class {
10556
11862
  * construction and is shown/hidden with the cluster container.
10557
11863
  */
10558
11864
  syncRowActions(row) {
10559
- const open = row.highlighted;
10560
- row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
11865
+ const open2 = row.highlighted;
11866
+ row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
10561
11867
  for (const child of Array.from(row.controlsEl.children)) {
10562
11868
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
10563
11869
  if (child === row.extrasEl) {
10564
- child.style.display = open ? "contents" : "none";
11870
+ child.style.display = open2 ? "contents" : "none";
10565
11871
  continue;
10566
11872
  }
10567
- child.style.display = open ? "inline-flex" : "none";
11873
+ child.style.display = open2 ? "inline-flex" : "none";
10568
11874
  }
10569
11875
  }
10570
11876
  /** Drop the current selection outline, if any. */
@@ -10618,7 +11924,8 @@ var InputsUI = class {
10618
11924
  /**
10619
11925
  * Open the indicator's settings as a centered modal over the chart — a themed,
10620
11926
  * dependency-free port of the app's tabbed script-settings dialog: a header with the
10621
- * indicator title, an "Inputs" tab, a scrollable body that groups inputs by `group=`
11927
+ * indicator title, a tab strip (the default "Inputs" tab plus any tab the inputs
11928
+ * declare via `tab=`), a scrollable body per tab that groups inputs by `group=`
10622
11929
  * (section headers) and packs `inline=` inputs onto one row, and a sticky footer.
10623
11930
  *
10624
11931
  * Edits commit LIVE (each change re-runs the indicator), matching the current
@@ -10648,55 +11955,57 @@ var InputsUI = class {
10648
11955
  });
10649
11956
  const card = document.createElement("div");
10650
11957
  card.className = "vela-ind-dialog";
10651
- card.style.cssText = `display:flex;flex-direction:column;width:fit-content;min-width:min(360px,90%);max-width:min(640px,94%);max-height:82%;background:${t.background};border:1px solid ${border};border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow-dialog);color:${fg};color-scheme:${scheme};font:13px ${font};overflow:hidden;`;
11958
+ card.style.cssText = `display:flex;flex-direction:column;width:fit-content;min-width:min(380px,90%);max-width:min(640px,94%);max-height:82%;background:${t.background};border:1px solid ${border};border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow-dialog);color:${fg};color-scheme:${scheme};font:14px ${font};overflow:hidden;`;
10652
11959
  if (this.mobileLayout) card.style.cssText += "width:100%;min-width:0;max-width:none;height:100%;max-height:none;border:none;border-radius:0;";
10653
11960
  applyChromeTokens(card, t);
10654
11961
  card.addEventListener("keydown", (e) => {
10655
11962
  if (e.key !== "Escape") e.stopPropagation();
10656
11963
  });
10657
11964
  const header = document.createElement("div");
10658
- header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 12px;flex:0 0 auto;";
11965
+ header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 20px;flex:0 0 auto;";
10659
11966
  const hTitle = document.createElement("span");
10660
11967
  hTitle.textContent = row.settingsTitle;
10661
- hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
11968
+ hTitle.style.cssText = "font-weight:600;font-size:20px;line-height:28px;";
10662
11969
  const closeBtn = document.createElement("button");
10663
11970
  closeBtn.type = "button";
10664
11971
  closeBtn.setAttribute("aria-label", "Close");
10665
11972
  this.dialogTips.push(attachChromeTooltip(closeBtn, { host: this.container, theme: () => this.theme, text: () => "Close" }));
10666
11973
  closeBtn.innerHTML = iconAt("close", 15);
10667
- closeBtn.className = "vela-ind-ctl";
10668
- closeBtn.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:2px;flex:0 0 auto;";
11974
+ closeBtn.className = "vela-ind-dlg-close";
10669
11975
  closeBtn.addEventListener("click", () => this.closeDialog());
10670
11976
  header.append(hTitle, closeBtn);
10671
11977
  card.appendChild(header);
10672
11978
  if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
11979
+ const tabDefs = tabInputs(row.inputs);
10673
11980
  const tabs = document.createElement("div");
10674
- tabs.style.cssText = `display:flex;gap:16px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
10675
- const tab = document.createElement("div");
10676
- tab.textContent = "Inputs";
10677
- tab.style.cssText = `padding:8px 2px;margin-bottom:-1px;border-bottom:2px solid ${fg};font-weight:600;font-size:13px;`;
10678
- tabs.appendChild(tab);
10679
- card.appendChild(tabs);
10680
- const body = document.createElement("div");
10681
- body.style.cssText = "padding:14px 20px 6px;overflow-y:auto;flex:1 1 auto;display:flex;flex-direction:column;gap:24px;";
10682
- for (const group of groupInputs(row.inputs)) {
10683
- const section = document.createElement("div");
10684
- section.style.cssText = "display:flex;flex-direction:column;gap:14px;";
10685
- if (group.name) {
10686
- const gh = document.createElement("div");
10687
- gh.textContent = group.name;
10688
- gh.style.cssText = "font-size:var(--vela-font-size-sm);font-weight:600;letter-spacing:0.05em;text-transform:uppercase;color:var(--vela-fg-muted);";
10689
- section.appendChild(gh);
11981
+ tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
11982
+ const tabEls = [];
11983
+ const bodies = [];
11984
+ const activate = (idx) => {
11985
+ for (let i = 0; i < tabEls.length; i += 1) {
11986
+ const active = i === idx;
11987
+ const el = tabEls[i];
11988
+ el.style.borderBottomColor = active ? fg : "transparent";
11989
+ el.style.color = active ? "inherit" : "var(--vela-fg-muted)";
11990
+ el.classList.toggle("vela-ind-tab-active", active);
11991
+ bodies[i].style.display = active ? "grid" : "none";
10690
11992
  }
10691
- const grid = document.createElement("div");
10692
- grid.style.cssText = this.mobileLayout ? "display:grid;grid-template-columns:minmax(0,1fr) auto auto max-content;align-items:center;column-gap:12px;row-gap:22px;" : "display:grid;grid-template-columns:minmax(140px,auto) auto auto 1fr;align-items:center;column-gap:12px;row-gap:22px;";
10693
- for (const inputRow of group.rows) this.buildInputRow(grid, inputRow, row);
10694
- section.appendChild(grid);
10695
- body.appendChild(section);
11993
+ };
11994
+ for (const [idx, def] of tabDefs.entries()) {
11995
+ const tab = document.createElement("div");
11996
+ tab.textContent = def.name;
11997
+ tab.className = "vela-ind-tab";
11998
+ tab.style.cssText = `padding:0 0 8px;margin-bottom:-1px;border-bottom:2px solid transparent;font-weight:600;font-size:16px;line-height:24px;${tabDefs.length > 1 ? "cursor:pointer;" : ""}`;
11999
+ tab.addEventListener("click", () => activate(idx));
12000
+ tabs.appendChild(tab);
12001
+ tabEls.push(tab);
12002
+ bodies.push(this.buildTabBody(def.inputs, row));
10696
12003
  }
10697
- card.appendChild(body);
12004
+ card.appendChild(tabs);
12005
+ for (const body of bodies) card.appendChild(body);
12006
+ activate(0);
10698
12007
  const footer = document.createElement("div");
10699
- footer.style.cssText = `display:flex;justify-content:flex-end;gap:8px;padding:14px 20px;border-top:1px solid ${border};flex:0 0 auto;`;
12008
+ footer.style.cssText = `display:flex;justify-content:flex-end;gap:12px;padding:16px 20px;border-top:1px solid ${border};flex:0 0 auto;`;
10700
12009
  footer.append(
10701
12010
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
10702
12011
  this.dialogButton("Ok", true, () => this.closeDialog())
@@ -10708,8 +12017,32 @@ var InputsUI = class {
10708
12017
  this.backdrop = backdrop;
10709
12018
  document.addEventListener("keydown", this.onDialogKey);
10710
12019
  }
12020
+ /** One tab's scrollable body — one shared grid so every group's controls share a
12021
+ * left edge (a 100px number field lines up with a wider session/time pair). */
12022
+ buildTabBody(inputs, row) {
12023
+ const body = document.createElement("div");
12024
+ body.style.cssText = this.mobileLayout ? "padding:16px 20px;overflow-y:auto;overflow-x:hidden;overflow-anchor:none;flex:1 1 auto;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;column-gap:16px;row-gap:16px;" : "padding:16px 20px;overflow-y:auto;overflow-x:hidden;overflow-anchor:none;flex:1 1 auto;display:grid;grid-template-columns:max-content 1fr;align-items:center;column-gap:16px;row-gap:16px;";
12025
+ for (const group of groupInputs(inputs)) {
12026
+ const section = document.createElement("div");
12027
+ section.style.cssText = "display:contents;";
12028
+ if (group.name) {
12029
+ const gh = document.createElement("div");
12030
+ gh.textContent = group.name;
12031
+ const first = body.childElementCount === 0;
12032
+ gh.style.cssText = `grid-column:1 / -1;font-size:11px;font-weight:600;letter-spacing:0.05em;text-transform:uppercase;color:var(--vela-fg-muted);padding:${first ? "4px" : "20px"} 0 8px;`;
12033
+ section.appendChild(gh);
12034
+ }
12035
+ for (const inputRow of group.rows) this.buildInputRow(section, inputRow, row);
12036
+ body.appendChild(section);
12037
+ }
12038
+ return body;
12039
+ }
10711
12040
  closeDialog() {
10712
12041
  document.removeEventListener("keydown", this.onDialogKey);
12042
+ closeOpenPopovers();
12043
+ this.choicePop = null;
12044
+ this.calendarPop = null;
12045
+ this.calendarAnchor = null;
10713
12046
  for (const dispose of this.dialogTips.splice(0)) dispose();
10714
12047
  this.backdrop?.remove();
10715
12048
  this.backdrop = null;
@@ -10737,79 +12070,77 @@ var InputsUI = class {
10737
12070
  buildInputRow(grid, decls, row) {
10738
12071
  const idOf = (inp) => `vela-inp-${row.id}-${inp.key}`;
10739
12072
  if (decls.length > 1) {
10740
- const cell = document.createElement("div");
10741
- cell.style.cssText = "grid-column:1 / -1;display:flex;flex-wrap:wrap;align-items:center;gap:12px;";
12073
+ const cell2 = document.createElement("div");
12074
+ cell2.style.cssText = "grid-column:1 / -1;display:flex;flex-wrap:wrap;align-items:center;gap:12px;";
10742
12075
  for (const d of decls) {
10743
12076
  const fit2 = d.type === "color" || d.type === "bool";
10744
12077
  const toggleFirst = d.type === "bool";
10745
12078
  const id2 = idOf(d);
10746
12079
  const item = document.createElement("div");
10747
- item.style.cssText = `display:flex;align-items:center;gap:6px;${toggleFirst ? "flex-direction:row-reverse;" : ""}`;
12080
+ item.style.cssText = `display:flex;align-items:center;gap:8px;${toggleFirst ? "flex-direction:row-reverse;" : ""}`;
10748
12081
  const name = nameOf(d);
10749
12082
  if (name) {
10750
12083
  const lbl = document.createElement("label");
10751
12084
  lbl.htmlFor = id2;
10752
12085
  lbl.textContent = name;
10753
- lbl.style.cssText = `font-size:13px;opacity:0.9;${toggleFirst ? "cursor:pointer;" : ""}`;
12086
+ lbl.style.cssText = `font-size:14px;opacity:0.9;${toggleFirst ? "cursor:pointer;" : ""}`;
10754
12087
  item.appendChild(lbl);
10755
12088
  }
10756
12089
  const cw2 = document.createElement("div");
10757
- cw2.style.cssText = fit2 ? "" : "width:110px;";
12090
+ cw2.style.cssText = fit2 ? "" : "width:100px;";
10758
12091
  cw2.appendChild(this.buildControl(row, d, id2));
10759
12092
  item.appendChild(cw2);
10760
- cell.appendChild(item);
12093
+ cell2.appendChild(item);
10761
12094
  }
10762
12095
  const lastTip = [...decls].reverse().find((d) => d.tooltip)?.tooltip;
10763
- if (lastTip) cell.appendChild(this.infoButton(lastTip));
10764
- grid.appendChild(cell);
12096
+ if (lastTip) cell2.appendChild(this.infoButton(lastTip));
12097
+ grid.appendChild(cell2);
10765
12098
  return;
10766
12099
  }
10767
12100
  const lead = decls[0];
10768
12101
  const id = idOf(lead);
10769
12102
  if (lead.type === "bool") {
10770
- const cell = document.createElement("div");
10771
- cell.style.cssText = "grid-column:1 / 4;display:flex;align-items:center;gap:8px;";
12103
+ const cell2 = document.createElement("div");
12104
+ cell2.style.cssText = "grid-column:1 / -1;display:flex;align-items:center;gap:8px;";
10772
12105
  const lbl = document.createElement("label");
10773
- lbl.style.cssText = "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;";
12106
+ lbl.style.cssText = "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;";
10774
12107
  lbl.appendChild(this.buildControl(row, lead, id));
10775
12108
  lbl.appendChild(document.createTextNode(nameOf(lead)));
10776
- cell.appendChild(lbl);
10777
- if (lead.tooltip) cell.appendChild(this.infoButton(lead.tooltip));
10778
- grid.appendChild(cell);
10779
- grid.appendChild(document.createElement("div"));
12109
+ cell2.appendChild(lbl);
12110
+ if (lead.tooltip) cell2.appendChild(this.infoButton(lead.tooltip));
12111
+ grid.appendChild(cell2);
10780
12112
  return;
10781
12113
  }
10782
12114
  if (lead.type === "text_area") {
10783
- const cell = document.createElement("div");
10784
- cell.style.cssText = "grid-column:1 / -1;display:flex;flex-direction:column;gap:8px;";
12115
+ const cell2 = document.createElement("div");
12116
+ cell2.style.cssText = "grid-column:1 / -1;display:flex;flex-direction:column;gap:8px;";
10785
12117
  const head = document.createElement("div");
10786
12118
  head.style.cssText = "display:flex;align-items:center;justify-content:center;gap:8px;";
10787
12119
  const lbl = document.createElement("label");
10788
12120
  lbl.htmlFor = id;
10789
12121
  lbl.textContent = nameOf(lead);
10790
- lbl.style.cssText = "font-size:13px;";
12122
+ lbl.style.cssText = "font-size:14px;";
10791
12123
  head.appendChild(lbl);
10792
12124
  if (lead.tooltip) head.appendChild(this.infoButton(lead.tooltip));
10793
- cell.appendChild(head);
10794
- cell.appendChild(this.buildControl(row, lead, id));
10795
- grid.appendChild(cell);
12125
+ cell2.appendChild(head);
12126
+ cell2.appendChild(this.buildControl(row, lead, id));
12127
+ grid.appendChild(cell2);
10796
12128
  return;
10797
12129
  }
10798
12130
  const label = document.createElement("label");
10799
12131
  label.htmlFor = id;
10800
12132
  label.textContent = nameOf(lead);
10801
- label.style.cssText = "font-size:13px;opacity:0.9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
10802
- const controlCell = document.createElement("div");
10803
- controlCell.style.cssText = "grid-column:span 2;display:flex;align-items:center;gap:8px;";
12133
+ label.style.cssText = "font-size:14px;opacity:0.9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
10804
12134
  const fit = lead.type === "color" || lead.type === "session" || lead.type === "time";
12135
+ const cell = document.createElement("div");
12136
+ cell.style.cssText = "display:flex;align-items:center;gap:8px;justify-self:start;";
10805
12137
  const cw = document.createElement("div");
10806
- cw.style.cssText = fit ? "" : "width:140px;";
12138
+ cw.style.cssText = fit ? "" : "width:100px;";
10807
12139
  cw.appendChild(this.buildControl(row, lead, id));
10808
- controlCell.appendChild(cw);
10809
- if (lead.tooltip) controlCell.appendChild(this.infoButton(lead.tooltip));
12140
+ cell.appendChild(cw);
12141
+ if (lead.tooltip) cell.appendChild(this.infoButton(lead.tooltip));
10810
12142
  grid.appendChild(label);
10811
- grid.appendChild(controlCell);
10812
- grid.appendChild(document.createElement("div"));
12143
+ grid.appendChild(cell);
10813
12144
  }
10814
12145
  /** Build the typed control for one input, committing edits live via `onChange`. */
10815
12146
  buildControl(row, inp, id) {
@@ -10818,107 +12149,30 @@ var InputsUI = class {
10818
12149
  row.values[inp.key] = value;
10819
12150
  this.onChange?.({ indicatorId: row.id, key: inp.key, value });
10820
12151
  };
10821
- const ctrl = this.ctrlStyle();
10822
12152
  if (inp.type === "bool") return this.buildToggle(id, Boolean(current), emit);
10823
- if (inp.options && inp.options.length > 0) return this.select(id, inp.options.map(String), String(current), `${ctrl}cursor:pointer;`, emit);
10824
- if (inp.type === "source") return this.select(id, SOURCES, String(current), `${ctrl}cursor:pointer;`, emit);
12153
+ if (inp.options && inp.options.length > 0) return this.select(id, inp.options.map(String), String(current), emit);
12154
+ if (inp.type === "source") return this.select(id, SOURCES, String(current), emit);
10825
12155
  if (inp.type === "color") {
10826
- const ci = document.createElement("input");
10827
- ci.type = "color";
10828
- ci.id = id;
10829
- ci.value = toHex6(String(current));
10830
- ci.style.cssText = `flex:0 0 auto;box-sizing:border-box;width:32px;height:32px;padding:2px;border:1px solid ${this.neutralBorder()};border-radius:0;background:transparent;cursor:pointer;`;
10831
- ci.addEventListener("input", () => emit(ci.value));
10832
- return ci;
12156
+ const field = colorField(this.theme, () => String(row.values[inp.key] ?? inp.defval), (v) => emit(v), { shape: "circle", id });
12157
+ return field;
10833
12158
  }
10834
12159
  if (inp.type === "symbol") return this.buildSymbol(id, String(current), emit);
10835
- if (inp.type === "timeframe") return this.selectPairs(id, TIMEFRAME_OPTIONS, String(current), `${ctrl}cursor:pointer;`, emit);
12160
+ if (inp.type === "timeframe") return this.selectPairs(id, TIMEFRAME_OPTIONS, String(current), emit);
10836
12161
  if (inp.type === "session") return this.buildSession(id, String(current), emit);
10837
12162
  if (inp.type === "time") return this.buildTime(id, Number(current) || 0, emit);
10838
12163
  if (inp.type === "text_area") return this.buildTextArea(id, String(current), emit);
10839
12164
  if (inp.type === "int" || inp.type === "float" || inp.type === "price") {
10840
- const ni = document.createElement("input");
10841
- ni.type = "number";
10842
- ni.id = id;
10843
- ni.value = String(current);
10844
- if (inp.min !== void 0) ni.min = String(inp.min);
10845
- if (inp.max !== void 0) ni.max = String(inp.max);
10846
- ni.step = String(inp.step ?? (inp.type === "int" ? 1 : 0.1));
10847
- ni.style.cssText = ctrl;
10848
- let last = String(current);
10849
- const commit = () => {
10850
- let n = Number(ni.value);
10851
- if (!Number.isFinite(n)) {
10852
- ni.value = last;
10853
- return;
10854
- }
10855
- if (inp.min !== void 0) n = Math.max(inp.min, n);
10856
- if (inp.max !== void 0) n = Math.min(inp.max, n);
10857
- if (inp.type === "int") n = Math.round(n);
10858
- ni.value = String(n);
10859
- if (String(n) !== last) {
10860
- last = String(n);
10861
- emit(n);
10862
- }
10863
- };
10864
- ni.addEventListener("blur", commit);
10865
- ni.addEventListener("keydown", (e) => {
10866
- if (e.key === "Enter") {
10867
- e.preventDefault();
10868
- ni.blur();
10869
- }
10870
- });
10871
- return ni;
12165
+ return this.buildNumber(id, Number(current), inp, emit);
10872
12166
  }
10873
12167
  return this.buildTextField(id, String(current), emit);
10874
12168
  }
10875
12169
  /** A plain text field that commits on blur / Enter (shared by string inputs and the pickerless symbol field). */
10876
12170
  buildTextField(id, current, emit) {
10877
- const ti = document.createElement("input");
10878
- ti.type = "text";
10879
- ti.id = id;
10880
- ti.value = current;
10881
- ti.style.cssText = this.ctrlStyle();
10882
- let last = current;
10883
- ti.addEventListener("blur", () => {
10884
- if (ti.value !== last) {
10885
- last = ti.value;
10886
- emit(ti.value);
10887
- }
10888
- });
10889
- ti.addEventListener("keydown", (e) => {
10890
- if (e.key === "Enter") {
10891
- e.preventDefault();
10892
- ti.blur();
10893
- }
10894
- });
10895
- return ti;
12171
+ return new TextField({ id, value: current, size: "md", onChange: emit }).el;
10896
12172
  }
10897
12173
  /** A square check-toggle (filled + check when on) — replaces the raw checkbox. */
10898
12174
  buildToggle(id, checked, onChange) {
10899
- const t = this.theme;
10900
- const border = this.neutralBorder();
10901
- const fill = this.strongText();
10902
- const b = document.createElement("button");
10903
- b.type = "button";
10904
- b.id = id;
10905
- b.setAttribute("role", "switch");
10906
- b.innerHTML = CHECK_SVG;
10907
- b.style.cssText = `width:20px;height:20px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;border:1px solid ${border};border-radius:5px;cursor:pointer;padding:0;transition:background .12s ease,border-color .12s ease;`;
10908
- let on = checked;
10909
- const paint = (v) => {
10910
- b.setAttribute("aria-checked", v ? "true" : "false");
10911
- b.style.background = v ? fill : "transparent";
10912
- b.style.borderColor = v ? fill : border;
10913
- b.style.color = v ? t.background : "transparent";
10914
- };
10915
- paint(checked);
10916
- b.addEventListener("click", () => {
10917
- on = !on;
10918
- paint(on);
10919
- onChange(on);
10920
- });
10921
- return b;
12175
+ return new Switch({ id, checked, size: "md", tone: "bright", onChange }).el;
10922
12176
  }
10923
12177
  /** A hoverable ⓘ affordance carrying an input's `tooltip` (themed chrome tip; wraps). */
10924
12178
  infoButton(tooltip2) {
@@ -10930,45 +12184,22 @@ var InputsUI = class {
10930
12184
  return b;
10931
12185
  }
10932
12186
  /** A dropdown over plain string options (value === label) — a thin case of {@link selectPairs}. */
10933
- select(id, options, current, style, emit) {
10934
- return this.selectPairs(id, options.map((o) => ({ value: o, label: o })), current, style, emit);
12187
+ select(id, options, current, emit) {
12188
+ return this.selectPairs(id, options.map((o) => ({ value: o, label: o })), current, emit);
10935
12189
  }
10936
12190
  /** A dropdown whose visible labels differ from the committed values (label ≠ value pairs). */
10937
- selectPairs(id, pairs, current, style, onChange) {
10938
- const sel = document.createElement("select");
10939
- sel.id = id;
10940
- sel.style.cssText = style;
10941
- if (current !== "" && !pairs.some((p) => p.value === current)) {
10942
- const o = document.createElement("option");
10943
- o.value = current;
10944
- o.textContent = current;
10945
- o.selected = true;
10946
- o.disabled = true;
10947
- sel.appendChild(o);
10948
- }
10949
- for (const p of pairs) {
10950
- const o = document.createElement("option");
10951
- o.value = p.value;
10952
- o.textContent = p.label;
10953
- if (p.value === current) o.selected = true;
10954
- sel.appendChild(o);
10955
- }
10956
- this.styleSelect(sel);
10957
- sel.addEventListener("change", () => onChange(sel.value));
10958
- return this.wrapSelectChevron(sel);
10959
- }
10960
- /** Wrap a `<select>` with an inset chevron — native arrows can't be repositioned reliably. */
10961
- wrapSelectChevron(sel) {
10962
- const wrap = document.createElement("div");
10963
- wrap.style.cssText = "position:relative;width:100%;";
10964
- sel.style.paddingRight = "28px";
10965
- const chevron = document.createElement("span");
10966
- chevron.innerHTML = SELECT_CHEVRON_SVG;
10967
- chevron.style.cssText = `position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none;display:flex;align-items:center;color:${this.strongText()};opacity:0.85;line-height:0;`;
10968
- wrap.append(sel, chevron);
10969
- return wrap;
12191
+ selectPairs(id, pairs, current, onChange) {
12192
+ return new Select({
12193
+ id,
12194
+ options: pairs,
12195
+ value: current,
12196
+ size: "md",
12197
+ theme: this.theme,
12198
+ list: this.listPopover(),
12199
+ onChange
12200
+ }).el;
10970
12201
  }
10971
- /** `input.session` → two HH:MM dropdowns committing a `HHMM-HHMM` session string. */
12202
+ /** `input.session` → two typeable HH:MM comboboxes committing a `HHMM-HHMM` session string. */
10972
12203
  buildSession(id, current, emit) {
10973
12204
  const [start, end] = sessionToTimes(current);
10974
12205
  const wrap = document.createElement("div");
@@ -10976,15 +12207,16 @@ var InputsUI = class {
10976
12207
  let from = start;
10977
12208
  let to = end;
10978
12209
  const commit = () => emit(timesToSession(from, to));
10979
- const startSel = this.timeSelect(id, from, 86, (v) => {
10980
- from = v;
10981
- commit();
10982
- });
10983
- const endSel = this.timeSelect(`${id}-end`, to, 86, (v) => {
10984
- to = v;
10985
- commit();
10986
- });
10987
- wrap.append(startSel, endSel);
12210
+ wrap.append(
12211
+ this.timeCombobox(id, from, 86, (v) => {
12212
+ from = v;
12213
+ commit();
12214
+ }),
12215
+ this.timeCombobox(`${id}-end`, to, 86, (v) => {
12216
+ to = v;
12217
+ commit();
12218
+ })
12219
+ );
10988
12220
  return wrap;
10989
12221
  }
10990
12222
  /** `input.time` → a date picker + an HH:MM dropdown, committing an epoch-ms timestamp. */
@@ -10999,20 +12231,15 @@ var InputsUI = class {
10999
12231
  const ms = Date.parse(`${date}T${time}:00`);
11000
12232
  if (Number.isFinite(ms)) emit(ms);
11001
12233
  };
11002
- const dateInput = document.createElement("input");
11003
- dateInput.type = "date";
11004
- dateInput.id = id;
11005
- dateInput.value = date;
11006
- dateInput.style.cssText = `${this.ctrlStyle()}width:auto;cursor:pointer;`;
11007
- dateInput.addEventListener("change", () => {
11008
- date = dateInput.value;
12234
+ const dateField = this.dateField(id, date, (v) => {
12235
+ date = v;
11009
12236
  commit();
11010
12237
  });
11011
- const timeSel = this.timeSelect(`${id}-time`, time, 86, (v) => {
12238
+ const timeSel = this.timeCombobox(`${id}-time`, time, 86, (v) => {
11012
12239
  time = v;
11013
12240
  commit();
11014
12241
  });
11015
- wrap.append(dateInput, timeSel);
12242
+ wrap.append(dateField, timeSel);
11016
12243
  return wrap;
11017
12244
  }
11018
12245
  /** `input.text_area` → a multi-line textarea, committed on blur. */
@@ -11031,9 +12258,261 @@ var InputsUI = class {
11031
12258
  });
11032
12259
  return ta;
11033
12260
  }
11034
- /** A fixed-width HH:MM dropdown (30-minute steps) used by the session + time controls. */
11035
- timeSelect(id, current, width, onChange) {
11036
- return this.selectPairs(id, TIME_OPTIONS, current, `${this.ctrlStyle()}width:${width}px;cursor:pointer;`, onChange);
12261
+ /** A fixed-width HH:MM combobox (typeable, plus a 30-minute-step dropdown) used by session + time. */
12262
+ timeCombobox(id, current, width, onChange) {
12263
+ const wrap = document.createElement("div");
12264
+ wrap.className = "vela-ind-combo";
12265
+ wrap.style.cssText = `position:relative;width:${width}px;`;
12266
+ const input = document.createElement("input");
12267
+ input.type = "text";
12268
+ input.id = id;
12269
+ input.value = current;
12270
+ input.autocomplete = "off";
12271
+ input.spellcheck = false;
12272
+ input.style.cssText = `${this.ctrlStyle()}padding-right:26px;`;
12273
+ const chevron = document.createElement("button");
12274
+ chevron.type = "button";
12275
+ chevron.tabIndex = -1;
12276
+ chevron.className = "vela-ind-combo-chevron";
12277
+ chevron.setAttribute("aria-label", "Open time list");
12278
+ chevron.innerHTML = CLOCK_SVG;
12279
+ let value = current;
12280
+ const commitTyped = () => {
12281
+ const next = normalizeTimeInput(input.value);
12282
+ if (!next) {
12283
+ input.value = value;
12284
+ return;
12285
+ }
12286
+ input.value = next;
12287
+ if (next !== value) {
12288
+ value = next;
12289
+ onChange(next);
12290
+ }
12291
+ };
12292
+ input.addEventListener("blur", () => {
12293
+ if (isPopoverOpen()) return;
12294
+ commitTyped();
12295
+ });
12296
+ const pick = (v) => {
12297
+ value = v;
12298
+ input.value = v;
12299
+ onChange(v);
12300
+ };
12301
+ const openList = () => {
12302
+ this.choicePop = toggleSelectList(wrap, TIME_OPTIONS, value, pick, {
12303
+ ...this.listPopover(),
12304
+ onClose: () => {
12305
+ this.choicePop = null;
12306
+ }
12307
+ });
12308
+ };
12309
+ input.addEventListener("keydown", (e) => {
12310
+ if (e.key === "Enter") {
12311
+ e.preventDefault();
12312
+ commitTyped();
12313
+ input.blur();
12314
+ }
12315
+ if (e.key === "ArrowDown") {
12316
+ e.preventDefault();
12317
+ openList();
12318
+ }
12319
+ });
12320
+ const open2 = (e) => {
12321
+ e.preventDefault();
12322
+ e.stopPropagation();
12323
+ openList();
12324
+ };
12325
+ input.addEventListener("click", open2);
12326
+ chevron.addEventListener("mousedown", (e) => e.preventDefault());
12327
+ chevron.addEventListener("click", open2);
12328
+ wrap.append(input, chevron);
12329
+ return wrap;
12330
+ }
12331
+ /** A typeable YYYY-MM-DD field that also opens a themed month calendar. */
12332
+ dateField(id, current, onChange) {
12333
+ const wrap = document.createElement("div");
12334
+ wrap.className = "vela-ind-combo";
12335
+ wrap.style.cssText = "position:relative;width:128px;";
12336
+ const input = document.createElement("input");
12337
+ input.type = "text";
12338
+ input.id = id;
12339
+ input.value = current;
12340
+ input.autocomplete = "off";
12341
+ input.spellcheck = false;
12342
+ input.style.cssText = `${this.ctrlStyle()}padding-right:26px;`;
12343
+ const chevron = document.createElement("button");
12344
+ chevron.type = "button";
12345
+ chevron.tabIndex = -1;
12346
+ chevron.className = "vela-ind-combo-chevron";
12347
+ chevron.setAttribute("aria-label", "Open calendar");
12348
+ chevron.innerHTML = CALENDAR_SVG;
12349
+ let value = current;
12350
+ const commitTyped = () => {
12351
+ const next = normalizeDateInput(input.value);
12352
+ if (!next) {
12353
+ input.value = value;
12354
+ return;
12355
+ }
12356
+ input.value = next;
12357
+ if (next !== value) {
12358
+ value = next;
12359
+ onChange(next);
12360
+ }
12361
+ };
12362
+ input.addEventListener("blur", () => {
12363
+ if (isPopoverOpen()) return;
12364
+ commitTyped();
12365
+ });
12366
+ const pick = (v) => {
12367
+ value = v;
12368
+ input.value = v;
12369
+ onChange(v);
12370
+ };
12371
+ input.addEventListener("keydown", (e) => {
12372
+ if (e.key === "Enter") {
12373
+ e.preventDefault();
12374
+ commitTyped();
12375
+ input.blur();
12376
+ }
12377
+ if (e.key === "ArrowDown") {
12378
+ e.preventDefault();
12379
+ this.openCalendar(wrap, value, pick);
12380
+ }
12381
+ });
12382
+ const open2 = (e) => {
12383
+ e.preventDefault();
12384
+ e.stopPropagation();
12385
+ if (openPopoverTrigger() === wrap) {
12386
+ closeOpenPopovers();
12387
+ return;
12388
+ }
12389
+ this.openCalendar(wrap, value, pick);
12390
+ };
12391
+ input.addEventListener("click", open2);
12392
+ chevron.addEventListener("mousedown", (e) => e.preventDefault());
12393
+ chevron.addEventListener("click", open2);
12394
+ wrap.append(input, chevron);
12395
+ return wrap;
12396
+ }
12397
+ /** Numeric field with hover steppers on the right (no fill until a triangle is hovered). */
12398
+ buildNumber(id, current, inp, emit) {
12399
+ return new NumberInput({
12400
+ id,
12401
+ value: current,
12402
+ min: inp.min,
12403
+ max: inp.max,
12404
+ step: inp.step ?? (inp.type === "int" ? 1 : 0.1),
12405
+ integer: inp.type === "int",
12406
+ size: "md",
12407
+ commit: "blur",
12408
+ onChange: emit
12409
+ }).el;
12410
+ }
12411
+ /** Placement shared by the indicator dialog's dropdowns and the time combobox. */
12412
+ listPopover() {
12413
+ return {
12414
+ theme: this.theme,
12415
+ matchWidth: true,
12416
+ boundary: this.dialog ?? "viewport",
12417
+ boundaryInset: 8,
12418
+ gap: 4
12419
+ };
12420
+ }
12421
+ /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
12422
+ openCalendar(anchor, current, onPick) {
12423
+ const parsed = parseIsoDate(current);
12424
+ let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
12425
+ let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
12426
+ const selected = parsed ? isoDate(parsed) : current;
12427
+ const pop = new Popover({
12428
+ trigger: anchor,
12429
+ theme: this.theme,
12430
+ className: "vela-ind-cal",
12431
+ boundary: this.dialog ?? "viewport",
12432
+ boundaryInset: 8,
12433
+ gap: 4,
12434
+ onClose: () => {
12435
+ this.calendarPop = null;
12436
+ this.calendarAnchor = null;
12437
+ },
12438
+ content: (el) => {
12439
+ const title = document.createElement("div");
12440
+ title.className = "vela-ind-cal-title";
12441
+ const prev2 = document.createElement("button");
12442
+ prev2.type = "button";
12443
+ prev2.className = "vela-ind-cal-nav";
12444
+ prev2.setAttribute("aria-label", "Previous month");
12445
+ prev2.innerHTML = iconAt("chevron-left", 14);
12446
+ const next = document.createElement("button");
12447
+ next.type = "button";
12448
+ next.className = "vela-ind-cal-nav";
12449
+ next.setAttribute("aria-label", "Next month");
12450
+ next.innerHTML = iconAt("chevron-right", 14);
12451
+ const head = document.createElement("div");
12452
+ head.className = "vela-ind-cal-head";
12453
+ head.append(prev2, title, next);
12454
+ const week = document.createElement("div");
12455
+ week.className = "vela-ind-cal-week";
12456
+ for (const d of WEEKDAY_LABELS) {
12457
+ const cell = document.createElement("span");
12458
+ cell.textContent = d;
12459
+ week.appendChild(cell);
12460
+ }
12461
+ const grid = document.createElement("div");
12462
+ grid.className = "vela-ind-cal-grid";
12463
+ const paint = () => {
12464
+ title.textContent = `${MONTH_LABELS[month]} ${year}`;
12465
+ grid.replaceChildren();
12466
+ const first = new Date(year, month, 1);
12467
+ const startPad = first.getDay();
12468
+ const days = new Date(year, month + 1, 0).getDate();
12469
+ const today = isoDate(/* @__PURE__ */ new Date());
12470
+ for (let i = 0; i < startPad; i++) {
12471
+ const blank = document.createElement("span");
12472
+ blank.className = "vela-ind-cal-blank";
12473
+ grid.appendChild(blank);
12474
+ }
12475
+ for (let day = 1; day <= days; day++) {
12476
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
12477
+ const b = document.createElement("button");
12478
+ b.type = "button";
12479
+ b.className = "vela-ind-cal-day";
12480
+ b.textContent = String(day);
12481
+ if (iso === selected) b.dataset.checked = "1";
12482
+ if (iso === today) b.dataset.today = "1";
12483
+ b.addEventListener("click", (e) => {
12484
+ e.stopPropagation();
12485
+ pop.hide();
12486
+ onPick(iso);
12487
+ });
12488
+ grid.appendChild(b);
12489
+ }
12490
+ };
12491
+ prev2.addEventListener("click", (e) => {
12492
+ e.stopPropagation();
12493
+ month -= 1;
12494
+ if (month < 0) {
12495
+ month = 11;
12496
+ year -= 1;
12497
+ }
12498
+ paint();
12499
+ });
12500
+ next.addEventListener("click", (e) => {
12501
+ e.stopPropagation();
12502
+ month += 1;
12503
+ if (month > 11) {
12504
+ month = 0;
12505
+ year += 1;
12506
+ }
12507
+ paint();
12508
+ });
12509
+ paint();
12510
+ el.append(head, week, grid);
12511
+ }
12512
+ });
12513
+ this.calendarPop = pop;
12514
+ this.calendarAnchor = anchor;
12515
+ pop.show();
11037
12516
  }
11038
12517
  /**
11039
12518
  * `input.symbol` → a field that opens the host's ticker-selection UI when a picker is wired
@@ -11061,7 +12540,7 @@ var InputsUI = class {
11061
12540
  }
11062
12541
  /** Shared field chrome for text / number / select controls (fills its wrapper's width). */
11063
12542
  ctrlStyle() {
11064
- return `width:100%;box-sizing:border-box;height:32px;background:transparent;border:1px solid ${this.neutralBorder()};color:${this.strongText()};border-radius:6px;padding:0 8px;font-size:13px;font-family:inherit;outline:none;`;
12543
+ return `width:100%;box-sizing:border-box;height:34px;background:transparent;border:1px solid var(--vela-border-strong);color:${this.strongText()};border-radius:6px;padding:0 8px;font-size:14px;font-family:inherit;outline:none;`;
11065
12544
  }
11066
12545
  /** Whether the active theme is dark (drives the dialog's `color-scheme`). */
11067
12546
  isDarkTheme() {
@@ -11080,21 +12559,6 @@ var InputsUI = class {
11080
12559
  strongText() {
11081
12560
  return "var(--vela-fg-bright)";
11082
12561
  }
11083
- /**
11084
- * Paint a `<select>` + its `<option>`s so the native dropdown popup matches the dialog: the
11085
- * option list's background is the dialog color and its text is the strong foreground. `color-scheme`
11086
- * alone doesn't theme the popup in every embedded browser (some render it white with white text —
11087
- * invisible), so the colors are set explicitly here.
11088
- */
11089
- styleSelect(sel) {
11090
- const bg = this.theme.background;
11091
- const fg = this.strongText();
11092
- sel.style.backgroundColor = bg;
11093
- for (const o of Array.from(sel.options)) {
11094
- o.style.backgroundColor = bg;
11095
- o.style.color = fg;
11096
- }
11097
- }
11098
12562
  dialogButton(label, primary, onClick) {
11099
12563
  const b = document.createElement("button");
11100
12564
  b.type = "button";
@@ -11108,6 +12572,21 @@ function nameOf(inp) {
11108
12572
  const t = inp.title;
11109
12573
  return t.length > 0 ? t.charAt(0).toUpperCase() + t.slice(1) : "";
11110
12574
  }
12575
+ var DEFAULT_INPUT_TAB = "Inputs";
12576
+ function tabInputs(inputs) {
12577
+ const byTab = /* @__PURE__ */ new Map();
12578
+ for (const inp of inputs) {
12579
+ const name = inp.tab && inp.tab.length > 0 ? inp.tab : DEFAULT_INPUT_TAB;
12580
+ if (!byTab.has(name)) byTab.set(name, []);
12581
+ byTab.get(name).push(inp);
12582
+ }
12583
+ const names = [...byTab.keys()];
12584
+ if (names.includes(DEFAULT_INPUT_TAB)) {
12585
+ names.splice(names.indexOf(DEFAULT_INPUT_TAB), 1);
12586
+ names.unshift(DEFAULT_INPUT_TAB);
12587
+ }
12588
+ return names.map((name) => ({ name, inputs: byTab.get(name) }));
12589
+ }
11111
12590
  function groupInputs(inputs) {
11112
12591
  const order = [];
11113
12592
  const byGroup = /* @__PURE__ */ new Map();
@@ -11136,9 +12615,10 @@ function groupInputs(inputs) {
11136
12615
  });
11137
12616
  }
11138
12617
  var CHECK_SVG = iconAt("check", 12);
11139
- var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
12618
+ var CALENDAR_SVG = iconAt("calendar", 14);
12619
+ var CLOCK_SVG = iconAt("clock", 14);
11140
12620
  var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
11141
- var DIALOG_STYLE_REV = "10";
12621
+ var DIALOG_STYLE_REV = "25";
11142
12622
  function ensureDialogStyles() {
11143
12623
  if (typeof document === "undefined") return;
11144
12624
  const existing = document.getElementById(DIALOG_STYLE_ID);
@@ -11147,30 +12627,47 @@ function ensureDialogStyles() {
11147
12627
  s.id = DIALOG_STYLE_ID;
11148
12628
  s.dataset.rev = DIALOG_STYLE_REV;
11149
12629
  s.textContent = `
11150
- .vela-ind-dialog input[type=color]{-webkit-appearance:none;appearance:none;}
11151
- .vela-ind-dialog input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
11152
- .vela-ind-dialog input[type=color]::-webkit-color-swatch{border:none;border-radius:4px;}
11153
- .vela-ind-dialog input[type=color]::-moz-color-swatch{border:none;border-radius:4px;}
11154
- .vela-ind-dialog input[type=text],.vela-ind-dialog input[type=number],.vela-ind-dialog select{transition:border-color .12s ease,box-shadow .12s ease;}
11155
- .vela-ind-dialog select{-webkit-appearance:none;appearance:none;}
11156
- .vela-ind-dialog input[type=text]:focus,.vela-ind-dialog input[type=number]:focus,.vela-ind-dialog select:focus{border-color:var(--vela-focus);box-shadow:0 0 0 3px var(--vela-focus-soft);}
11157
- .vela-ind-dialog ::-webkit-scrollbar{width:9px;}
12630
+ .vela-ind-dialog input[type=text],.vela-ind-dialog input[type=number],.vela-ind-dialog input[type=date]{transition:border-color .12s ease,box-shadow .12s ease;}
12631
+ .vela-ind-dialog input[type=text]:hover,.vela-ind-dialog input[type=number]:hover,.vela-ind-dialog input[type=date]:hover{border-color:var(--vela-fg-muted);}
12632
+ .vela-ind-dialog input[type=text]:focus,.vela-ind-dialog input[type=number]:focus,.vela-ind-dialog input[type=date]:focus{border-color:var(--vela-focus);box-shadow:0 0 0 3px var(--vela-focus-soft);}
12633
+ .vela-ind-combo-chevron{position:absolute;right:0;top:0;bottom:0;width:26px;border:none;background:transparent;color:inherit;opacity:0.55;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}
12634
+ .vela-ind-combo-chevron:hover{opacity:0.9;}
12635
+ .vela-ind-cal{background:var(--vela-bg);color:var(--vela-fg);border:none;border-radius:6px;box-shadow:var(--vela-shadow);font:14px var(--vela-font);padding:10px 12px;user-select:none;}
12636
+ .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
12637
+ .vela-ind-cal-title{flex:1;text-align:center;font-weight:600;font-size:14px;color:var(--vela-fg-bright);}
12638
+ .vela-ind-cal-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}
12639
+ .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
12640
+ .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
12641
+ .vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
12642
+ .vela-ind-cal-week span{line-height:20px;}
12643
+ .vela-ind-cal-blank{width:28px;height:28px;}
12644
+ .vela-ind-cal-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
12645
+ .vela-ind-cal-day:hover{background:var(--vela-hover);}
12646
+ .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
12647
+ .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
12648
+ .vela-ind-dialog ::-webkit-scrollbar{width:9px;height:9px;}
11158
12649
  .vela-ind-dialog ::-webkit-scrollbar-thumb{background:var(--vela-scroll);border-radius:4px;border:2px solid transparent;background-clip:padding-box;}
11159
12650
  .vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
12651
+ .vela-ind-dialog ::-webkit-scrollbar-button{display:none;width:0;height:0;}
12652
+ .vela-ind-tab{transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
12653
+ .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
11160
12654
  .vela-ind-hint{background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
11161
12655
  .vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
11162
12656
  .vela-ind-ctl,.vela-ind-close{background-color:transparent;color:inherit;transition:color var(--vela-dur-fast) ease,background-color var(--vela-dur-fast) ease;}
11163
12657
  .vela-ind-ctl svg,.vela-ind-close svg{width:${LEGEND_ICON_PX}px;height:${LEGEND_ICON_PX}px;display:block;flex:none;stroke-width:1;}
11164
12658
  .vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
11165
12659
  .vela-ind-close:hover{color:var(--vela-danger) !important;background-color:var(--vela-hover-strong);}
12660
+ .vela-ind-dlg-close{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg-muted);line-height:0;width:30px;height:30px;flex:0 0 auto;border-radius:var(--vela-radius-sm);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
12661
+ .vela-ind-dlg-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
12662
+ .vela-ind-dlg-close svg{width:15px;height:15px;display:block;flex:none;}
11166
12663
  .vela-ind-fold{transition:border-color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
11167
12664
  .vela-ind-fold:hover{border-color:var(--vela-border-strong);opacity:0.9;}
11168
12665
  .vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
11169
12666
  .vela-ind-menuitem:hover{background:var(--vela-hover-strong);}
11170
- .vela-ind-btn{cursor:pointer;padding:7px 14px;border-radius:var(--vela-radius-md);border:1px solid transparent;background:transparent;color:var(--vela-fg-muted);font-weight:600;font-size:13px;font-family:inherit;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
11171
- .vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
11172
- .vela-ind-btn-primary{padding:7px 16px;border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
11173
- .vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;}`;
12667
+ .vela-ind-btn{cursor:pointer;height:34px;padding:0 11px;border-radius:6px;border:1px solid var(--vela-border-strong);background:transparent;color:var(--vela-fg);font-weight:400;font-size:14px;font-family:inherit;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
12668
+ .vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);border-color:var(--vela-fg-muted);}
12669
+ .vela-ind-btn-primary{border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
12670
+ .vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;border-color:var(--vela-selected-bg);}`;
11174
12671
  if (!existing) document.head.appendChild(s);
11175
12672
  }
11176
12673
  var TIMEFRAME_OPTIONS = [
@@ -11195,6 +12692,37 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
11195
12692
  const v = `${hh}:${mm}`;
11196
12693
  return { value: v, label: v };
11197
12694
  });
12695
+ var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
12696
+ var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
12697
+ function normalizeDateInput(raw) {
12698
+ const t = raw.trim();
12699
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
12700
+ if (!m) return null;
12701
+ const y = Number(m[1]);
12702
+ const mo = Number(m[2]);
12703
+ const d = Number(m[3]);
12704
+ const dt = new Date(y, mo - 1, d);
12705
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
12706
+ return isoDate(dt);
12707
+ }
12708
+ function parseIsoDate(raw) {
12709
+ const iso = normalizeDateInput(raw);
12710
+ if (!iso) return null;
12711
+ const [y, mo, d] = iso.split("-").map(Number);
12712
+ return new Date(y, mo - 1, d);
12713
+ }
12714
+ function isoDate(d) {
12715
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
12716
+ }
12717
+ function normalizeTimeInput(raw) {
12718
+ const t = raw.trim();
12719
+ const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
12720
+ if (!m) return null;
12721
+ const hh = Number(m[1]);
12722
+ const mm = Number(m[2]);
12723
+ if (hh > 23 || mm > 59) return null;
12724
+ return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
12725
+ }
11198
12726
  function sessionToTimes(session) {
11199
12727
  const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(session.trim());
11200
12728
  if (!m) return ["09:00", "16:00"];
@@ -15301,6 +16829,12 @@ function extendEndpoints(x1, y1, x2, y2, extend, width, height) {
15301
16829
  const rx = right ? width + 2 : Math.max(x1, x2);
15302
16830
  return [lx, yAt(lx), rx, yAt(rx)];
15303
16831
  }
16832
+ function uprightLineAngle(x1, y1, x2, y2) {
16833
+ let a = Math.atan2(y2 - y1, x2 - x1);
16834
+ if (a > Math.PI / 2) a -= Math.PI;
16835
+ if (a < -Math.PI / 2) a += Math.PI;
16836
+ return a;
16837
+ }
15304
16838
  function parseColor2(c) {
15305
16839
  const s = c.trim();
15306
16840
  let m = /^#([0-9a-f]{3})$/i.exec(s);
@@ -16555,380 +18089,71 @@ function tzButtonLabel(zone) {
16555
18089
  return tzOffset(zone);
16556
18090
  }
16557
18091
 
16558
- // src/core/drawings/effectiveColor.ts
16559
- var VALID_FILL = VALID;
16560
- var INVALID_FILL = INVALID;
16561
- function effectiveFillColor(d, theme) {
16562
- if (d instanceof PatternDrawing) {
16563
- if (!d.fillTriangles().length) return null;
16564
- const valid = d.valid();
16565
- return d.style.fillColor ?? (valid == null ? d.style.lineColor : valid ? VALID_FILL : INVALID_FILL);
16566
- }
16567
- if (d instanceof CalloutBase) return d.style.fillColor ?? theme.background;
16568
- return d.style.fillColor ?? null;
16569
- }
16570
-
16571
- // src/renderers/native/drawings/colorPicker.ts
16572
- function splitColor(color) {
16573
- const s = String(color ?? "").trim();
16574
- let m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
16575
- if (m) return { hex6: `#${m[1].toLowerCase()}`, alpha: m[2] ? parseInt(m[2], 16) / 255 : 1 };
16576
- m = /^#([0-9a-f]{3})$/i.exec(s);
16577
- if (m) {
16578
- const h = m[1].toLowerCase();
16579
- return { hex6: `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`, alpha: 1 };
16580
- }
16581
- const rgba = /^rgba?\(([^)]+)\)/i.exec(s);
16582
- if (rgba) {
16583
- const p = rgba[1].split(",").map((v) => v.trim());
16584
- const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
16585
- const a = p[3] != null ? Math.max(0, Math.min(1, parseFloat(p[3]))) : 1;
16586
- return { hex6: `#${hx(parseInt(p[0] ?? "0", 10))}${hx(parseInt(p[1] ?? "0", 10))}${hx(parseInt(p[2] ?? "0", 10))}`, alpha: a };
16587
- }
16588
- return { hex6: DEFAULT_DRAWING_COLOR, alpha: 1 };
16589
- }
16590
- function combineColor(hex6, alpha) {
16591
- const a = Math.max(0, Math.min(1, alpha));
16592
- if (a >= 0.999) return hex6;
16593
- return `${hex6}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
16594
- }
16595
- function blendOver(fg, bg, alpha) {
16596
- const rgb = (c) => {
16597
- const n = parseInt(splitColor(c).hex6.slice(1), 16);
16598
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
16599
- };
16600
- const [fr, fgc, fb] = rgb(fg);
16601
- const [br, bgc, bb] = rgb(bg);
16602
- const a = Math.max(0, Math.min(1, alpha));
16603
- const h = (n) => Math.round(n).toString(16).padStart(2, "0");
16604
- return `#${h(fr * a + br * (1 - a))}${h(fgc * a + bgc * (1 - a))}${h(fb * a + bb * (1 - a))}`;
16605
- }
16606
- function hslHex(h, s, l) {
16607
- const sn = s / 100;
16608
- const ln = l / 100;
16609
- const k = (n) => (n + h / 30) % 12;
16610
- const a = sn * Math.min(ln, 1 - ln);
16611
- const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
16612
- const to = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
16613
- return `#${to(f(0))}${to(f(8))}${to(f(4))}`;
16614
- }
16615
- function buildPalette() {
16616
- const grays = [100, 90, 80, 68, 56, 45, 35, 25, 14, 0].map((l) => hslHex(0, 0, l));
16617
- const hues = [4, 28, 50, 96, 140, 174, 200, 224, 270, 322];
16618
- const levels = [
16619
- [82, 56],
16620
- // vivid
16621
- [58, 84],
16622
- // light pastel
16623
- [62, 74],
16624
- [66, 62],
16625
- [70, 50],
16626
- [64, 39],
16627
- [54, 29]
16628
- // dark
16629
- ];
16630
- return [grays, ...levels.map(([s, l]) => hues.map((h) => hslHex(h, s, l)))];
16631
- }
16632
- var PALETTE = buildPalette();
16633
- function transparencyChecker(size) {
16634
- return `repeating-conic-gradient(#9aa0a6 0% 25%, #d3d6da 0% 50%) 0 0 / ${size}px ${size}px`;
16635
- }
16636
- var CHECKER = transparencyChecker(10);
16637
- var recents = [ACCENT, BULLISH, BEARISH, WARNING, NEUTRAL];
16638
- function addRecent(hex6) {
16639
- const i = recents.findIndex((c) => c.toLowerCase() === hex6.toLowerCase());
16640
- if (i >= 0) recents.splice(i, 1);
16641
- recents.unshift(hex6);
16642
- if (recents.length > 10) recents.length = 10;
16643
- }
16644
- function buildColorPicker(color, theme, onChange) {
16645
- const parsed = splitColor(color);
16646
- let curHex = parsed.hex6;
16647
- let curAlpha = parsed.alpha;
16648
- const root = document.createElement("div");
16649
- root.style.cssText = "display:flex;flex-direction:column;gap:9px;width:236px;";
16650
- const grid = document.createElement("div");
16651
- grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
16652
- const swatches = [];
16653
- for (const row of PALETTE) {
16654
- for (const c of row) {
16655
- const sw = document.createElement("button");
16656
- sw.type = "button";
16657
- sw.dataset.c = c;
16658
- sw.style.cssText = `width:100%;aspect-ratio:1;border-radius:4px;border:none;cursor:pointer;background:${c};padding:0;`;
16659
- sw.addEventListener("click", (e) => {
16660
- e.stopPropagation();
16661
- pickHex(c);
16662
- });
16663
- grid.appendChild(sw);
16664
- swatches.push(sw);
16665
- }
16666
- }
16667
- const paintSelection = () => {
16668
- for (const sw of swatches) {
16669
- const on = (sw.dataset.c ?? "").toLowerCase() === curHex.toLowerCase();
16670
- sw.style.outline = on ? `2px solid ${theme.textColor}` : "none";
16671
- sw.style.outlineOffset = "2px";
16672
- sw.style.zIndex = on ? "1" : "";
16673
- sw.style.position = on ? "relative" : "";
16674
- }
16675
- };
16676
- const recentRow = document.createElement("div");
16677
- recentRow.style.cssText = "display:flex;align-items:center;gap:5px;flex-wrap:wrap;border-top:1px solid var(--vela-border);padding-top:9px;";
16678
- const renderRecents = () => {
16679
- recentRow.replaceChildren();
16680
- for (const c of recents) {
16681
- const sw = document.createElement("button");
16682
- sw.type = "button";
16683
- sw.style.cssText = `width:17px;height:17px;border-radius:var(--vela-radius-sm);border:1px solid var(--vela-border);cursor:pointer;background:${c};padding:0;`;
16684
- sw.addEventListener("click", (e) => {
16685
- e.stopPropagation();
16686
- pickHex(c);
16687
- });
16688
- recentRow.appendChild(sw);
16689
- }
16690
- const add = document.createElement("label");
16691
- add.style.cssText = `width:17px;height:17px;border-radius:var(--vela-radius-sm);border:1px dashed var(--vela-border-strong);cursor:pointer;display:flex;align-items:center;justify-content:center;color:${theme.textColor};font:14px ${theme.fontFamily};position:relative;`;
16692
- add.textContent = "+";
16693
- const input = document.createElement("input");
16694
- input.type = "color";
16695
- input.value = curHex;
16696
- input.style.cssText = "position:absolute;inset:0;opacity:0;cursor:pointer;";
16697
- input.addEventListener("input", () => pickHex(input.value, true));
16698
- add.appendChild(input);
16699
- recentRow.appendChild(add);
16700
- };
16701
- const opLabel = document.createElement("div");
16702
- opLabel.textContent = "Opacity";
16703
- opLabel.style.cssText = `font:11px ${theme.fontFamily};color:var(--vela-fg-muted);`;
16704
- const opRow = document.createElement("div");
16705
- opRow.style.cssText = "display:flex;align-items:center;gap:10px;";
16706
- const track = document.createElement("div");
16707
- track.style.cssText = "flex:1;position:relative;height:13px;border-radius:7px;cursor:pointer;";
16708
- const knob = document.createElement("div");
16709
- knob.style.cssText = "position:absolute;top:50%;width:15px;height:15px;border-radius:50%;background:var(--vela-selected-bg);box-shadow:0 1px 3px rgba(0,0,0,0.55);transform:translate(-50%,-50%);pointer-events:none;";
16710
- track.appendChild(knob);
16711
- const pctBox = document.createElement("div");
16712
- pctBox.style.cssText = `min-width:42px;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;`;
16713
- opRow.append(track, pctBox);
16714
- const paintOpacity = () => {
16715
- track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
16716
- knob.style.left = `${curAlpha * 100}%`;
16717
- pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
16718
- };
16719
- let dragging = false;
16720
- const onDrag = (clientX) => {
16721
- const r = track.getBoundingClientRect();
16722
- curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
16723
- paintOpacity();
16724
- emit();
16725
- };
16726
- track.addEventListener("pointerdown", (e) => {
16727
- e.stopPropagation();
16728
- dragging = true;
16729
- track.setPointerCapture(e.pointerId);
16730
- onDrag(e.clientX);
16731
- });
16732
- track.addEventListener("pointermove", (e) => {
16733
- if (dragging) onDrag(e.clientX);
16734
- });
16735
- track.addEventListener("pointerup", () => dragging = false);
16736
- function emit() {
16737
- onChange(combineColor(curHex, curAlpha));
16738
- }
16739
- function pickHex(hex, custom = false) {
16740
- curHex = splitColor(hex).hex6;
16741
- addRecent(curHex);
16742
- paintSelection();
16743
- paintOpacity();
16744
- if (custom) renderRecents();
16745
- emit();
16746
- }
16747
- renderRecents();
16748
- paintSelection();
16749
- paintOpacity();
16750
- root.append(grid, recentRow, opLabel, opRow);
16751
- return root;
16752
- }
16753
-
16754
- // src/renderers/native/chrome/ColorField.ts
16755
- var CHECKER2 = transparencyChecker(8);
16756
- var STYLE_ID2 = "vela-color-field";
16757
- function ensureStyles2() {
16758
- if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
16759
- const st = document.createElement("style");
16760
- st.id = STYLE_ID2;
16761
- st.textContent = `
16762
- .vela-color-field{width:24px;height:24px;padding:2px;border:1px solid var(--vela-border);border-radius:0;background:var(--vela-surface-sunken);cursor:pointer;display:inline-flex;flex:none;}
16763
- .vela-color-field:hover{border-color:var(--vela-fg-muted);}
16764
- .vela-color-field-swatch{display:block;width:100%;height:100%;border-radius:0;box-shadow:inset 0 0 0 1px rgba(0,0,0,0.25);}
16765
- .vela-color-field-pop{position:fixed;z-index:6000;background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:10px;}
16766
- `;
16767
- document.head.appendChild(st);
16768
- }
16769
- var openPopover = null;
16770
- function closeOpen() {
16771
- if (!openPopover) return;
16772
- document.removeEventListener("pointerdown", openPopover.onOutside, true);
16773
- window.removeEventListener("resize", openPopover.reflow, true);
16774
- openPopover.el.remove();
16775
- openPopover = null;
16776
- }
16777
- function place(pop, trigger) {
16778
- const r = trigger.getBoundingClientRect();
16779
- const w = pop.offsetWidth;
16780
- const h = pop.offsetHeight;
16781
- let left = r.right - w;
16782
- let top = r.bottom + 6;
16783
- if (left < 6) left = 6;
16784
- if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
16785
- if (top + h > window.innerHeight - 6) top = r.top - h - 6;
16786
- if (top < 6) top = 6;
16787
- pop.style.left = `${Math.round(left)}px`;
16788
- pop.style.top = `${Math.round(top)}px`;
16789
- }
16790
- function colorField(theme, getVal, onVal) {
16791
- ensureStyles2();
16792
- const trigger = document.createElement("button");
16793
- trigger.type = "button";
16794
- trigger.className = "vela-color-field";
16795
- const swatch = document.createElement("span");
16796
- swatch.className = "vela-color-field-swatch";
16797
- trigger.appendChild(swatch);
16798
- const paint = () => {
16799
- const v = getVal();
16800
- swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
16801
- };
16802
- paint();
16803
- trigger.addEventListener("vela-sync", paint);
16804
- trigger.addEventListener("click", (e) => {
16805
- e.stopPropagation();
16806
- if (openPopover && openPopover.trigger === trigger) {
16807
- closeOpen();
16808
- return;
16809
- }
16810
- closeOpen();
16811
- const pop = document.createElement("div");
16812
- pop.className = "vela-color-field-pop";
16813
- applyChromeTokens(pop, theme);
16814
- pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
16815
- pop.appendChild(
16816
- buildColorPicker(getVal(), theme, (val) => {
16817
- onVal(val);
16818
- paint();
16819
- })
16820
- );
16821
- document.body.appendChild(pop);
16822
- place(pop, trigger);
16823
- const onOutside = (ev) => {
16824
- const t = ev.target;
16825
- if (pop.contains(t) || trigger.contains(t)) return;
16826
- closeOpen();
16827
- };
16828
- const reflow = () => place(pop, trigger);
16829
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
16830
- window.addEventListener("resize", reflow, true);
16831
- openPopover = { el: pop, trigger, onOutside, reflow };
16832
- });
16833
- return trigger;
16834
- }
16835
- function closeColorPopover() {
16836
- closeOpen();
16837
- }
16838
-
16839
18092
  // src/renderers/native/chrome/WidthField.ts
16840
18093
  var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
16841
- var STYLE_ID3 = "vela-width-field";
16842
- function ensureStyles3() {
16843
- if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
16844
- const st = document.createElement("style");
16845
- st.id = STYLE_ID3;
16846
- st.textContent = `
18094
+ var STYLE_ID2 = "vela-width-field";
18095
+ var WIDTH_CSS = `
16847
18096
  .vela-width-field{height:28px;padding:0 8px;border:1px solid var(--vela-border-strong);border-radius:var(--vela-radius-sm);background:var(--vela-surface-elev);cursor:pointer;display:inline-flex;align-items:center;gap:8px;flex:none;color:var(--vela-fg);font-family:inherit;outline:none;}
16848
18097
  .vela-width-field:hover{border-color:var(--vela-fg-muted);}
16849
- .vela-width-field-pop{position:fixed;z-index:6000;background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;display:flex;flex-direction:column;gap:1px;color:var(--vela-fg);}
18098
+ .vela-width-field-caret{display:flex;color:var(--vela-fg-muted);}
18099
+ .vela-width-field-pop{background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;display:flex;flex-direction:column;gap:1px;color:var(--vela-fg);}
16850
18100
  .vela-width-field-item{display:flex;align-items:center;gap:8px;min-width:96px;padding:5px 8px;border:none;border-radius:5px;background:transparent;color:inherit;cursor:pointer;text-align:left;font:inherit;}
16851
18101
  .vela-width-field-item:hover{background:var(--vela-hover-strong);}
16852
18102
  .vela-width-field-item[data-active='1']{background:var(--vela-active);}
16853
18103
  `;
16854
- document.head.appendChild(st);
16855
- }
16856
18104
  function lineGlyph(width) {
16857
18105
  return `<svg width="22" height="14" viewBox="0 0 22 14" fill="none"><line x1="2" y1="7" x2="20" y2="7" stroke="currentColor" stroke-width="${width}" stroke-linecap="round"/></svg>`;
16858
18106
  }
16859
- var CHEVRON = `<svg width="8" height="5" viewBox="0 0 8 5" fill="none"><path d="M1 1l3 3 3-3" stroke="#868a96" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
16860
- var openPopover2 = null;
16861
- function closeOpen2() {
16862
- if (!openPopover2) return;
16863
- document.removeEventListener("pointerdown", openPopover2.onOutside, true);
16864
- window.removeEventListener("resize", openPopover2.reflow, true);
16865
- openPopover2.el.remove();
16866
- openPopover2 = null;
16867
- }
16868
- function place2(pop, trigger) {
16869
- const r = trigger.getBoundingClientRect();
16870
- const w = pop.offsetWidth;
16871
- const h = pop.offsetHeight;
16872
- let left = r.right - w;
16873
- let top = r.bottom + 6;
16874
- if (left < 6) left = 6;
16875
- if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
16876
- if (top + h > window.innerHeight - 6) top = r.top - h - 6;
16877
- if (top < 6) top = 6;
16878
- pop.style.left = `${Math.round(left)}px`;
16879
- pop.style.top = `${Math.round(top)}px`;
16880
- }
18107
+ var CHEVRON = `<svg width="8" height="5" viewBox="0 0 8 5" fill="none"><path d="M1 1l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
16881
18108
  function widthField(theme, getVal, onVal) {
16882
- ensureStyles3();
18109
+ injectStyles(STYLE_ID2, WIDTH_CSS, document);
16883
18110
  const trigger = document.createElement("button");
16884
18111
  trigger.type = "button";
16885
18112
  trigger.className = "vela-width-field";
16886
18113
  const paint = () => {
16887
- trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
18114
+ trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span class="vela-width-field-caret">${CHEVRON}</span>`;
16888
18115
  };
16889
18116
  paint();
18117
+ trigger.addEventListener("pointerdown", (e) => {
18118
+ e.preventDefault();
18119
+ trigger.focus({ preventScroll: true });
18120
+ });
16890
18121
  trigger.addEventListener("click", (e) => {
16891
18122
  e.stopPropagation();
16892
- if (openPopover2 && openPopover2.trigger === trigger) {
16893
- closeOpen2();
18123
+ if (openPopoverTrigger() === trigger) {
18124
+ closeOpenPopovers();
16894
18125
  return;
16895
18126
  }
16896
- closeOpen2();
16897
- const pop = document.createElement("div");
16898
- pop.className = "vela-width-field-pop";
16899
- applyChromeTokens(pop, theme);
16900
- pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
16901
- pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
16902
- for (const w of WIDTH_FIELD_OPTIONS) {
16903
- const item = document.createElement("button");
16904
- item.type = "button";
16905
- item.className = "vela-width-field-item";
16906
- item.dataset.active = w === getVal() ? "1" : "0";
16907
- item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
16908
- item.addEventListener("click", (ev) => {
16909
- ev.stopPropagation();
16910
- closeOpen2();
16911
- onVal(w);
16912
- paint();
16913
- });
16914
- pop.appendChild(item);
16915
- }
16916
- document.body.appendChild(pop);
16917
- place2(pop, trigger);
16918
- const onOutside = (ev) => {
16919
- const t = ev.target;
16920
- if (pop.contains(t) || trigger.contains(t)) return;
16921
- closeOpen2();
16922
- };
16923
- const reflow = () => place2(pop, trigger);
16924
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
16925
- window.addEventListener("resize", reflow, true);
16926
- openPopover2 = { el: pop, trigger, onOutside, reflow };
18127
+ const pop = new Popover({
18128
+ trigger,
18129
+ theme,
18130
+ align: "end",
18131
+ gap: 6,
18132
+ className: "vela-width-field-pop",
18133
+ content: (el) => {
18134
+ el.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
18135
+ for (const w of WIDTH_FIELD_OPTIONS) {
18136
+ const item = document.createElement("button");
18137
+ item.type = "button";
18138
+ item.className = "vela-width-field-item";
18139
+ item.dataset.active = w === getVal() ? "1" : "0";
18140
+ item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
18141
+ item.addEventListener("click", (ev) => {
18142
+ ev.stopPropagation();
18143
+ pop.hide();
18144
+ onVal(w);
18145
+ paint();
18146
+ });
18147
+ el.appendChild(item);
18148
+ }
18149
+ }
18150
+ });
18151
+ pop.show();
16927
18152
  });
16928
18153
  return trigger;
16929
18154
  }
16930
18155
  function closeWidthPopover() {
16931
- closeOpen2();
18156
+ closeOpenPopovers();
16932
18157
  }
16933
18158
 
16934
18159
  // src/renderers/native/chrome/SettingsDialog.ts
@@ -16943,37 +18168,24 @@ function styleLabel(id) {
16943
18168
  return chartType(id)?.label ?? BUILTIN_STYLE_LABELS[id] ?? id;
16944
18169
  }
16945
18170
  var SD_STYLE_ID = "vela-settings-controls";
18171
+ var SD_STYLE_REV = "2";
16946
18172
  var SETTINGS_SURFACE = "var(--vela-surface)";
16947
18173
  var SETTINGS_BORDER = "var(--vela-border)";
16948
18174
  var SETTINGS_TEXT = "var(--vela-fg)";
16949
18175
  function ensureControlStyles() {
16950
- if (typeof document === "undefined" || document.getElementById(SD_STYLE_ID)) return;
16951
- const st = document.createElement("style");
18176
+ if (typeof document === "undefined") return;
18177
+ const existing = document.getElementById(SD_STYLE_ID);
18178
+ if (existing?.dataset.rev === SD_STYLE_REV) return;
18179
+ const st = existing ?? document.createElement("style");
16952
18180
  st.id = SD_STYLE_ID;
18181
+ st.dataset.rev = SD_STYLE_REV;
16953
18182
  st.textContent = `
16954
- .vela-sd-check{width:18px;height:18px;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0;border:1px solid var(--vela-border-strong);border-radius:5px;background:transparent;color:transparent;cursor:pointer;}
16955
- .vela-sd-check:hover{border-color:var(--vela-fg-muted);}
16956
- .vela-sd-check.on{background:var(--vela-selected-bg);border-color:var(--vela-selected-bg);color:var(--vela-selected-fg);}
16957
- .vela-sd-check svg{display:block;}
16958
- .vela-sd-select,.vela-sd-number{height:28px;background:var(--vela-surface-elev);border:1px solid var(--vela-border-strong);border-radius:var(--vela-radius-sm);color:var(--vela-fg);padding:0 8px;font-size:13px;outline:none;font-family:inherit;}
16959
- .vela-sd-select:hover,.vela-sd-number:hover{border-color:var(--vela-fg-muted);}
16960
- /* Custom caret: the native one hugs the right border; ours gets 8px of air. (A data URI
16961
- can't read currentColor, so it uses the shared muted-gray ink, legible on both themes.) */
16962
- .vela-sd-select{-webkit-appearance:none;appearance:none;padding-right:26px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M1 1l3 3 3-3' fill='none' stroke='%23868a96' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;}
16963
- .vela-sd-number{width:64px;}
16964
- .vela-sd-color{width:32px;height:26px;padding:0;border:1px solid var(--vela-border);border-radius:var(--vela-radius-sm);background:transparent;cursor:pointer;-webkit-appearance:none;appearance:none;}
16965
- .vela-sd-color::-webkit-color-swatch-wrapper{padding:2px;}
16966
- .vela-sd-color::-webkit-color-swatch{border:none;border-radius:2px;}
16967
18183
  .vela-sd-pane::-webkit-scrollbar{width:8px;}
16968
18184
  .vela-sd-pane::-webkit-scrollbar-thumb{background:var(--vela-scroll);border-radius:var(--vela-radius-sm);border:2px solid transparent;background-clip:padding-box;}
16969
18185
  .vela-sd-pane::-webkit-scrollbar-track{background:transparent;}
16970
- .vela-sd-toggle{position:relative;width:38px;height:22px;border-radius:11px;background:var(--vela-surface-sunken);border:1px solid var(--vela-border);cursor:pointer;flex:none;padding:0;}
16971
- .vela-sd-toggle::after{content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--vela-fg-muted);transition:transform var(--vela-dur-med) ease,background var(--vela-dur-med) ease;}
16972
- .vela-sd-toggle.on{background:var(--vela-active);border-color:var(--vela-selected-bg);}
16973
- .vela-sd-toggle.on::after{transform:translateX(16px);background:var(--vela-selected-bg);}
16974
18186
  /* Tab rail / footer button / header close: base styles live HERE, not inline on the
16975
18187
  elements \u2014 inline declarations always beat stylesheet :hover rules, which is exactly
16976
- what killed these hovers before. Active tab state is the .on class (like .vela-sd-check),
18188
+ what killed these hovers before. Active tab state is the .on class,
16977
18189
  hover fills follow the app convention (--vela-hover + --vela-fg-bright, fast transition). */
16978
18190
  .vela-sd-tab{text-align:left;padding:9px 12px;background:transparent;border:none;border-radius:var(--vela-radius-md);color:var(--vela-fg-muted);font-weight:600;font-size:13px;font-family:inherit;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
16979
18191
  .vela-sd-tab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
@@ -17032,13 +18244,13 @@ function ensureControlStyles() {
17032
18244
  .vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
17033
18245
  .vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
17034
18246
  .vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
17035
- .vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
17036
- .vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
18247
+ .vela-sd-mobile .vela-switch{width:22px;height:22px;}
18248
+ .vela-sd-mobile .vela-select[data-size='sm'] .vela-select-trigger,.vela-sd-mobile .vela-num[data-size='sm'] input{height:34px;}
17037
18249
  .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
17038
18250
  .vela-sd-mobile .vela-sd-btn{height:38px;}
17039
18251
  .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
17040
18252
  `;
17041
- document.head.appendChild(st);
18253
+ if (!existing) document.head.appendChild(st);
17042
18254
  }
17043
18255
  var SettingsDialog = class {
17044
18256
  constructor(container, theme) {
@@ -17354,15 +18566,15 @@ var SettingsDialog = class {
17354
18566
  railScrim = document.createElement("div");
17355
18567
  railScrim.className = "vela-sd-railscrim";
17356
18568
  railScrim.addEventListener("click", () => toggleRail?.());
17357
- toggleRail = (open) => {
17358
- const on = open ?? !rail.classList.contains("open");
18569
+ toggleRail = (open2) => {
18570
+ const on = open2 ?? !rail.classList.contains("open");
17359
18571
  rail.classList.toggle("open", on);
17360
18572
  railScrim?.classList.toggle("open", on);
17361
18573
  };
17362
18574
  }
17363
18575
  const paneHost = document.createElement("div");
17364
18576
  paneHost.className = "vela-sd-pane";
17365
- paneHost.style.cssText = mobile ? "flex:1;min-width:0;overflow-y:auto;padding:6px 14px calc(14px + env(safe-area-inset-bottom, 0px));" : "flex:1;overflow-y:auto;padding:6px 18px 14px;";
18577
+ paneHost.style.cssText = mobile ? "flex:1;min-width:0;overflow-y:auto;overflow-anchor:none;padding:6px 14px calc(14px + env(safe-area-inset-bottom, 0px));" : "flex:1;overflow-y:auto;overflow-anchor:none;padding:6px 18px 14px;";
17366
18578
  const panes = [];
17367
18579
  let current = null;
17368
18580
  for (const child of [...body.children]) {
@@ -17428,7 +18640,7 @@ var SettingsDialog = class {
17428
18640
  }
17429
18641
  }
17430
18642
  close() {
17431
- closeColorPopover();
18643
+ closeOpenPopovers();
17432
18644
  closeWidthPopover();
17433
18645
  this.root?.remove();
17434
18646
  this.root = null;
@@ -17583,50 +18795,41 @@ var SettingsDialog = class {
17583
18795
  return wf;
17584
18796
  }
17585
18797
  if (c.kind === "select") {
17586
- const sel = document.createElement("select");
17587
- sel.className = "vela-sd-select";
17588
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
17589
- sel.title = c.label;
17590
18798
  const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
17591
- for (const [val, lbl] of normalizeSelectOptions(c.options)) {
17592
- const o = document.createElement("option");
17593
- o.value = val;
17594
- o.textContent = lbl;
17595
- if (val === current2) o.selected = true;
17596
- sel.appendChild(o);
17597
- }
17598
- sel.addEventListener("change", () => put(c.key, sel.value));
17599
- sel.addEventListener("vela-sync", () => {
17600
- sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
18799
+ const sel = new Select({
18800
+ options: normalizeSelectOptions(c.options).map(([value, label]) => ({ value, label })),
18801
+ value: current2,
18802
+ size: "sm",
18803
+ theme: this.theme,
18804
+ onChange: (v) => put(c.key, v)
17601
18805
  });
17602
- return sel;
17603
- }
17604
- const ni = document.createElement("input");
17605
- ni.type = "number";
17606
- ni.className = "vela-sd-number";
17607
- if (compact) ni.style.width = "56px";
17608
- if (c.min !== void 0) ni.min = String(c.min);
17609
- if (c.max !== void 0) ni.max = String(c.max);
17610
- ni.step = String(c.step ?? 1);
17611
- ni.title = c.label;
17612
- const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
17613
- if (c.placeholder !== void 0) {
17614
- ni.placeholder = c.placeholder;
17615
- if (current !== c.defval) ni.value = String(current);
17616
- ni.addEventListener("change", () => {
17617
- const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
17618
- const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
17619
- ni.value = v === c.defval ? "" : String(v);
17620
- put(c.key, v);
17621
- });
17622
- } else {
17623
- ni.value = String(current);
17624
- ni.addEventListener("input", () => {
17625
- const v = Number(ni.value);
17626
- if (Number.isFinite(v)) put(c.key, v);
18806
+ sel.el.title = c.label;
18807
+ sel.el.addEventListener("vela-sync", () => {
18808
+ sel.setValue(typeof bag[c.key] === "string" ? bag[c.key] : c.defval);
17627
18809
  });
18810
+ return sel.el;
17628
18811
  }
17629
- return ni;
18812
+ const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
18813
+ const ni = new NumberInput({
18814
+ value: current,
18815
+ min: c.min,
18816
+ max: c.max,
18817
+ step: c.step ?? 1,
18818
+ size: "sm",
18819
+ commit: "live",
18820
+ clamp: c.placeholder !== void 0,
18821
+ steppers: false,
18822
+ compact,
18823
+ title: c.label,
18824
+ placeholder: c.placeholder,
18825
+ emptyValue: c.placeholder !== void 0 ? c.defval : void 0,
18826
+ onChange: (v) => put(c.key, v)
18827
+ });
18828
+ ni.el.addEventListener("vela-sync", () => {
18829
+ const next = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
18830
+ ni.setValue(next);
18831
+ });
18832
+ return ni.el;
17630
18833
  }
17631
18834
  /**
17632
18835
  * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
@@ -17907,28 +19110,27 @@ var SettingsDialog = class {
17907
19110
  * only one visible at a time; see `typeRow`). */
17908
19111
  toggleRow(label, value, onToggle, controls, get) {
17909
19112
  const wrap = document.createElement("div");
17910
- const cb = document.createElement("button");
17911
- cb.type = "button";
17912
- cb.className = "vela-sd-check" + (value ? " on" : "");
17913
- cb.innerHTML = iconAt("check", 11);
17914
- let checked = value;
17915
- const cbToggle = () => {
17916
- checked = !checked;
17917
- cb.classList.toggle("on", checked);
17918
- return checked;
17919
- };
19113
+ const sw = new Switch({ checked: value, size: "sm", onChange: (v) => {
19114
+ onToggle(v);
19115
+ if (controls.length > 0) syncDim(v);
19116
+ } });
17920
19117
  const lbl = document.createElement("span");
17921
19118
  lbl.textContent = label;
17922
19119
  lbl.style.cssText = "opacity:0.85;white-space:nowrap;";
19120
+ const syncDim = (on) => {
19121
+ for (const c of controls) {
19122
+ if (c.dataset.sdSelfGated === "1") continue;
19123
+ c.style.opacity = on ? "1" : "0.4";
19124
+ c.style.pointerEvents = on ? "" : "none";
19125
+ }
19126
+ };
17923
19127
  if (controls.length === 0) {
17924
19128
  wrap.className = "vela-sd-bool";
17925
19129
  wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
17926
- wrap.append(cb, lbl);
17927
- cb.addEventListener("click", () => onToggle(cbToggle()));
19130
+ wrap.append(sw.el, lbl);
17928
19131
  if (get) {
17929
19132
  wrap.addEventListener("vela-sync", () => {
17930
- checked = get();
17931
- cb.classList.toggle("on", checked);
19133
+ sw.setChecked(get());
17932
19134
  });
17933
19135
  }
17934
19136
  return wrap;
@@ -17936,28 +19138,16 @@ var SettingsDialog = class {
17936
19138
  wrap.className = "vela-sd-row";
17937
19139
  const left = document.createElement("div");
17938
19140
  left.style.cssText = "display:flex;align-items:center;gap:8px;";
17939
- left.append(cb, lbl);
19141
+ left.append(sw.el, lbl);
17940
19142
  const box = document.createElement("div");
17941
19143
  box.style.cssText = "display:flex;align-items:center;gap:6px;";
17942
19144
  for (const c of controls) box.appendChild(c);
17943
- const syncDim = (on) => {
17944
- for (const c of controls) {
17945
- if (c.dataset.sdSelfGated === "1") continue;
17946
- c.style.opacity = on ? "1" : "0.4";
17947
- c.style.pointerEvents = on ? "" : "none";
17948
- }
17949
- };
17950
19145
  syncDim(value);
17951
- cb.addEventListener("click", () => {
17952
- const v = cbToggle();
17953
- onToggle(v);
17954
- syncDim(v);
17955
- });
17956
19146
  if (get) {
17957
19147
  wrap.addEventListener("vela-sync", () => {
17958
- checked = get();
17959
- cb.classList.toggle("on", checked);
17960
- syncDim(checked);
19148
+ const v = get();
19149
+ sw.setChecked(v);
19150
+ syncDim(v);
17961
19151
  });
17962
19152
  }
17963
19153
  wrap.append(left, box);
@@ -17965,52 +19155,23 @@ var SettingsDialog = class {
17965
19155
  }
17966
19156
  numberRow(label, value, min, max, step, onChange) {
17967
19157
  const { wrap } = this.row(label);
17968
- const ni = document.createElement("input");
17969
- ni.type = "number";
17970
- ni.value = String(value);
17971
- ni.min = String(min);
17972
- ni.max = String(max);
17973
- ni.step = String(step);
17974
- ni.className = "vela-sd-number";
17975
- ni.addEventListener("input", () => {
17976
- const n = Number(ni.value);
17977
- if (Number.isFinite(n)) onChange(n);
17978
- });
17979
- wrap.appendChild(ni);
19158
+ wrap.appendChild(new NumberInput({ value, min, max, step, size: "sm", commit: "live", onChange }).el);
17980
19159
  return wrap;
17981
19160
  }
17982
19161
  /** A dropdown whose option values differ from their display labels. */
17983
19162
  selectRowLabeled(label, value, options, onChange) {
17984
19163
  const { wrap } = this.row(label);
17985
- const sel = document.createElement("select");
17986
- sel.className = "vela-sd-select";
17987
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
17988
- for (const [val, lbl] of options) {
17989
- const o = document.createElement("option");
17990
- o.value = val;
17991
- o.textContent = lbl;
17992
- if (val === value) o.selected = true;
17993
- sel.appendChild(o);
17994
- }
17995
- sel.addEventListener("change", () => onChange(sel.value));
17996
- wrap.appendChild(sel);
19164
+ wrap.appendChild(new Select({
19165
+ options: options.map(([v, l]) => ({ value: v, label: l })),
19166
+ value,
19167
+ size: "sm",
19168
+ theme: this.theme,
19169
+ onChange
19170
+ }).el);
17997
19171
  return wrap;
17998
19172
  }
17999
19173
  selectRow(label, value, options, onChange) {
18000
- const { wrap } = this.row(label);
18001
- const sel = document.createElement("select");
18002
- sel.className = "vela-sd-select";
18003
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
18004
- for (const opt of options) {
18005
- const o = document.createElement("option");
18006
- o.value = opt;
18007
- o.textContent = opt;
18008
- if (opt === value) o.selected = true;
18009
- sel.appendChild(o);
18010
- }
18011
- sel.addEventListener("change", () => onChange(sel.value));
18012
- wrap.appendChild(sel);
18013
- return wrap;
19174
+ return this.selectRowLabeled(label, value, options.map((o) => [o, o]), onChange);
18014
19175
  }
18015
19176
  /** Footer with the full config as JSON — the export/import (templating) surface. */
18016
19177
  footer(_config) {
@@ -18035,6 +19196,19 @@ function timezoneOptions(current) {
18035
19196
  return [[normalized, tzMenuLabel(normalized, normalized)], ...options];
18036
19197
  }
18037
19198
 
19199
+ // src/core/drawings/effectiveColor.ts
19200
+ var VALID_FILL = VALID;
19201
+ var INVALID_FILL = INVALID;
19202
+ function effectiveFillColor(d, theme) {
19203
+ if (d instanceof PatternDrawing) {
19204
+ if (!d.fillTriangles().length) return null;
19205
+ const valid = d.valid();
19206
+ return d.style.fillColor ?? (valid == null ? d.style.lineColor : valid ? VALID_FILL : INVALID_FILL);
19207
+ }
19208
+ if (d instanceof CalloutBase) return d.style.fillColor ?? theme.background;
19209
+ return d.style.fillColor ?? null;
19210
+ }
19211
+
18038
19212
  // src/renderers/native/drawings/DrawingPainter.ts
18039
19213
  var HANDLE_RADIUS = 4.5;
18040
19214
  var HANDLE_BORDER = DEFAULT_DRAWING_COLOR;
@@ -18401,13 +19575,19 @@ var DrawingPainter = class {
18401
19575
  }
18402
19576
  this.paintLabel(ctx, d, proj, theme);
18403
19577
  }
18404
- /** Draw a drawing's optional text label at a type-appropriate anchor (multi-line aware). */
19578
+ /** Draw a drawing's optional text label at a type-appropriate anchor (multi-line aware).
19579
+ * Two-point line tools rotate the glyphs to follow the segment, kept upright. */
18405
19580
  paintLabel(ctx, d, proj, theme) {
18406
19581
  const text = d.text;
18407
19582
  if (!text || !text.value || d.id === this.targets.mutedLabel) return;
18408
19583
  const layout = labelLayout(d, proj);
18409
19584
  if (!layout) return;
18410
19585
  const fs = namedFontSize(text.size);
19586
+ ctx.save();
19587
+ if (layout.rotate) {
19588
+ ctx.translate(layout.rotate.cx, layout.rotate.cy);
19589
+ ctx.rotate(layout.rotate.angle);
19590
+ }
18411
19591
  ctx.font = `${text.bold ? "bold " : ""}${text.italic ? "italic " : ""}${fs}px ${theme.fontFamily}`;
18412
19592
  ctx.textBaseline = "top";
18413
19593
  ctx.textAlign = layout.align;
@@ -18415,8 +19595,8 @@ var DrawingPainter = class {
18415
19595
  const lh = labelLineHeight(fs);
18416
19596
  const lines = text.value.split("\n");
18417
19597
  lines.forEach((line, i) => ctx.fillText(line, layout.x, layout.top + i * lh));
18418
- ctx.textAlign = "left";
18419
19598
  if (d.type === "text") this.paintTextFrame(ctx, d.id, layout.x, layout.top, lines, fs, theme);
19599
+ ctx.restore();
18420
19600
  }
18421
19601
  /** Frame a plain text label while it is the target, so the text reads as a clickable object even
18422
19602
  * though it has no shape of its own: a firm frame once selected, a fainter one under the cursor.
@@ -19359,7 +20539,14 @@ function labelLayout(d, proj) {
19359
20539
  case "infoline":
19360
20540
  case "trendangle": {
19361
20541
  if (pts.length < 2) return null;
19362
- return { x: (pts[0][0] + pts[1][0]) / 2, top: (pts[0][1] + pts[1][1]) / 2 - 6 - lh * lines, align: "center" };
20542
+ const [x1, y1] = pts[0];
20543
+ const [x2, y2] = pts[1];
20544
+ return {
20545
+ x: 0,
20546
+ top: -6 - lh * lines,
20547
+ align: "center",
20548
+ rotate: { angle: uprightLineAngle(x1, y1, x2, y2), cx: (x1 + x2) / 2, cy: (y1 + y2) / 2 }
20549
+ };
19363
20550
  }
19364
20551
  case "box": {
19365
20552
  if (pts.length < 2) return null;
@@ -19708,11 +20895,11 @@ function applyFree(anchor, pt, free) {
19708
20895
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
19709
20896
  var BTN = 30;
19710
20897
  var ICON = 21;
19711
- var STYLE_ID4 = "vela-drawing-popup-styles";
19712
- function ensureStyles4() {
19713
- if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
20898
+ var STYLE_ID3 = "vela-drawing-popup-styles";
20899
+ function ensureStyles2() {
20900
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
19714
20901
  const s = document.createElement("style");
19715
- s.id = STYLE_ID4;
20902
+ s.id = STYLE_ID3;
19716
20903
  s.textContent = `
19717
20904
  .vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
19718
20905
  .vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -19746,17 +20933,14 @@ var DrawingSettingsPopup = class {
19746
20933
  this.textPanel = null;
19747
20934
  this.levelsPanel = null;
19748
20935
  this.colorPop = null;
19749
- // floating RGB + opacity picker for a color swatch
19750
20936
  this.colorOwner = null;
19751
- // the swatch the open color popover belongs to (for toggle)
19752
- this.menuEl = null;
19753
- // floating option list for a dropdown control
20937
+ this.menuPop = null;
19754
20938
  this.menuOwner = null;
19755
20939
  this.onClose = null;
19756
20940
  this.onOutside = (e) => {
19757
20941
  const target = e.target;
19758
- if (this.colorPop?.contains(target)) return;
19759
- if (this.menuEl?.contains(target)) return;
20942
+ if (this.colorPop?.el.contains(target)) return;
20943
+ if (this.menuPop?.el.contains(target)) return;
19760
20944
  if (this.el && !this.el.contains(target)) {
19761
20945
  const cb = this.onClose;
19762
20946
  this.close();
@@ -19775,13 +20959,13 @@ var DrawingSettingsPopup = class {
19775
20959
  * picker / dropdown menu), which live outside `el`. */
19776
20960
  contains(node) {
19777
20961
  if (!node) return false;
19778
- return this.el?.contains(node) === true || this.colorPop?.contains(node) === true || this.menuEl?.contains(node) === true;
20962
+ return this.el?.contains(node) === true || this.colorPop?.el.contains(node) === true || this.menuPop?.el.contains(node) === true;
19779
20963
  }
19780
20964
  /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
19781
20965
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
19782
20966
  open(drawing, anchor, actions, onClose) {
19783
20967
  this.close();
19784
- ensureStyles4();
20968
+ ensureStyles2();
19785
20969
  this.onClose = onClose ?? null;
19786
20970
  const t = this.theme;
19787
20971
  const schema = drawing.schema();
@@ -19792,8 +20976,7 @@ var DrawingSettingsPopup = class {
19792
20976
  el.style.cssText = `position:absolute;z-index:22;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};display:flex;flex-direction:column;pointer-events:auto;overflow:hidden;`;
19793
20977
  applyChromeTokens(el, t);
19794
20978
  el.addEventListener("pointerdown", () => {
19795
- this.closeColorPopover();
19796
- this.closeMenu();
20979
+ closeOpenPopovers();
19797
20980
  });
19798
20981
  const bar = document.createElement("div");
19799
20982
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
@@ -20530,49 +21713,76 @@ var DrawingSettingsPopup = class {
20530
21713
  });
20531
21714
  return b;
20532
21715
  }
21716
+ /** Host-anchored floating shell (stays inside the chart). Content is filled after
21717
+ * construction so `fill` can close over the live `Popover` without hitting TDZ. */
21718
+ hostFloat(anchor, opts) {
21719
+ const t = this.theme;
21720
+ const pop = new Popover({
21721
+ trigger: anchor,
21722
+ host: this.host,
21723
+ theme: t,
21724
+ position: "absolute",
21725
+ boundary: this.host,
21726
+ boundaryInset: 4,
21727
+ viewportInset: 0,
21728
+ gap: 6,
21729
+ align: opts.align ?? "start",
21730
+ zIndex: opts.zIndex,
21731
+ onClose: () => {
21732
+ if (this.menuPop === pop) {
21733
+ this.menuPop = null;
21734
+ this.menuOwner = null;
21735
+ }
21736
+ if (this.colorPop === pop) {
21737
+ this.colorPop = null;
21738
+ this.colorOwner = null;
21739
+ }
21740
+ }
21741
+ });
21742
+ const el = pop.el;
21743
+ el.style.background = t.background;
21744
+ el.style.border = "1px solid var(--vela-border)";
21745
+ el.style.borderRadius = "var(--vela-radius-lg)";
21746
+ el.style.boxShadow = "var(--vela-shadow)";
21747
+ el.style.padding = opts.padding;
21748
+ el.style.display = "flex";
21749
+ el.style.flexDirection = "column";
21750
+ el.style.gap = "1px";
21751
+ el.style.color = t.textColor;
21752
+ el.style.font = `var(--vela-font-size-md) ${t.fontFamily}`;
21753
+ el.style.pointerEvents = "auto";
21754
+ opts.fill(el, pop);
21755
+ pop.show();
21756
+ return pop;
21757
+ }
20533
21758
  /** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
20534
21759
  openActionMenu(anchor, rows) {
20535
- this.closeMenu();
20536
- this.closeColorPopover();
20537
- const t = this.theme;
20538
- const menu2 = document.createElement("div");
20539
- menu2.className = "vela-dpop";
20540
- menu2.style.cssText = `position:absolute;z-index:26;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;pointer-events:auto;display:flex;flex-direction:column;gap:1px;color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};`;
20541
- applyChromeTokens(menu2, t);
20542
- menu2.addEventListener("pointerdown", (e) => e.stopPropagation());
20543
- for (const row of rows) {
20544
- const item = document.createElement("button");
20545
- item.type = "button";
20546
- item.className = "vela-dpop-item";
20547
- item.style.cssText = "display:flex;align-items:center;gap:9px;min-width:150px;padding:6px 9px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;";
20548
- const ic = document.createElement("span");
20549
- ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
20550
- ic.innerHTML = sized(row.icon, 18);
20551
- const tx = document.createElement("span");
20552
- tx.textContent = row.label;
20553
- tx.style.cssText = "flex:1;";
20554
- item.append(ic, tx);
20555
- item.addEventListener("click", (e) => {
20556
- e.stopPropagation();
20557
- this.closeMenu();
20558
- row.onClick();
20559
- });
20560
- menu2.appendChild(item);
20561
- }
20562
- this.host.appendChild(menu2);
20563
- const ar = anchor.getBoundingClientRect();
20564
- const hr = this.host.getBoundingClientRect();
20565
- const w = menu2.offsetWidth;
20566
- const h = menu2.offsetHeight;
20567
- let left = ar.right - hr.left - w;
20568
- let top = ar.bottom - hr.top + 6;
20569
- if (left + w > hr.width - 4) left = hr.width - w - 4;
20570
- if (left < 4) left = 4;
20571
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
20572
- if (top < 4) top = 4;
20573
- menu2.style.left = `${Math.round(left)}px`;
20574
- menu2.style.top = `${Math.round(top)}px`;
20575
- this.menuEl = menu2;
21760
+ this.menuPop = this.hostFloat(anchor, {
21761
+ align: "end",
21762
+ zIndex: 26,
21763
+ padding: "4px",
21764
+ fill: (menu2, pop) => {
21765
+ for (const row of rows) {
21766
+ const item = document.createElement("button");
21767
+ item.type = "button";
21768
+ item.className = "vela-dpop-item";
21769
+ item.style.cssText = "display:flex;align-items:center;gap:9px;min-width:150px;padding:6px 9px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;";
21770
+ const ic = document.createElement("span");
21771
+ ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
21772
+ ic.innerHTML = sized(row.icon, 18);
21773
+ const tx = document.createElement("span");
21774
+ tx.textContent = row.label;
21775
+ tx.style.cssText = "flex:1;";
21776
+ item.append(ic, tx);
21777
+ item.addEventListener("click", (e) => {
21778
+ e.stopPropagation();
21779
+ pop.hide();
21780
+ row.onClick();
21781
+ });
21782
+ menu2.appendChild(item);
21783
+ }
21784
+ }
21785
+ });
20576
21786
  this.menuOwner = anchor;
20577
21787
  }
20578
21788
  /** The floating hover-label. Sits just above or below the toolbar depending on where it floats,
@@ -20702,60 +21912,40 @@ var DrawingSettingsPopup = class {
20702
21912
  /** The floating option list opened by a {@link dropdown} trigger. Each row shows the option's
20703
21913
  * glyph (and text label when one is supplied); the current value is highlighted. */
20704
21914
  openMenu(anchor, values, current, render, label, onPick) {
20705
- this.closeMenu();
20706
- this.closeColorPopover();
20707
- const t = this.theme;
20708
- const menu2 = document.createElement("div");
20709
- menu2.className = "vela-dpop";
20710
- menu2.style.cssText = `position:absolute;z-index:26;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;pointer-events:auto;display:flex;flex-direction:column;gap:1px;color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};`;
20711
- applyChromeTokens(menu2, t);
20712
- menu2.addEventListener("pointerdown", (e) => e.stopPropagation());
20713
- for (const v of values) {
20714
- const active = v === current;
20715
- const item = document.createElement("button");
20716
- item.type = "button";
20717
- item.className = "vela-dpop-item";
20718
- item.dataset.active = active ? "1" : "0";
20719
- item.style.cssText = `display:flex;align-items:center;gap:8px;${label ? "min-width:118px;" : ""}padding:5px 8px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;`;
20720
- const ic = document.createElement("span");
20721
- ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
20722
- ic.innerHTML = sized(render(v), 18);
20723
- item.appendChild(ic);
20724
- if (label) {
20725
- const tx = document.createElement("span");
20726
- tx.textContent = label(v);
20727
- tx.style.cssText = "flex:1;font-variant-numeric:tabular-nums;";
20728
- item.appendChild(tx);
21915
+ this.menuPop = this.hostFloat(anchor, {
21916
+ zIndex: 26,
21917
+ padding: "4px",
21918
+ fill: (menu2, pop) => {
21919
+ for (const v of values) {
21920
+ const active = v === current;
21921
+ const item = document.createElement("button");
21922
+ item.type = "button";
21923
+ item.className = "vela-dpop-item";
21924
+ item.dataset.active = active ? "1" : "0";
21925
+ item.style.cssText = `display:flex;align-items:center;gap:8px;${label ? "min-width:118px;" : ""}padding:5px 8px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;`;
21926
+ const ic = document.createElement("span");
21927
+ ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
21928
+ ic.innerHTML = sized(render(v), 18);
21929
+ item.appendChild(ic);
21930
+ if (label) {
21931
+ const tx = document.createElement("span");
21932
+ tx.textContent = label(v);
21933
+ tx.style.cssText = "flex:1;font-variant-numeric:tabular-nums;";
21934
+ item.appendChild(tx);
21935
+ }
21936
+ item.addEventListener("click", (e) => {
21937
+ e.stopPropagation();
21938
+ pop.hide();
21939
+ onPick(v);
21940
+ });
21941
+ menu2.appendChild(item);
21942
+ }
20729
21943
  }
20730
- item.addEventListener("click", (e) => {
20731
- e.stopPropagation();
20732
- this.closeMenu();
20733
- onPick(v);
20734
- });
20735
- menu2.appendChild(item);
20736
- }
20737
- this.host.appendChild(menu2);
20738
- const ar = anchor.getBoundingClientRect();
20739
- const hr = this.host.getBoundingClientRect();
20740
- const w = menu2.offsetWidth;
20741
- const h = menu2.offsetHeight;
20742
- let left = ar.left - hr.left;
20743
- let top = ar.bottom - hr.top + 6;
20744
- if (left + w > hr.width - 4) left = hr.width - w - 4;
20745
- if (left < 4) left = 4;
20746
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
20747
- if (top < 4) top = 4;
20748
- menu2.style.left = `${Math.round(left)}px`;
20749
- menu2.style.top = `${Math.round(top)}px`;
20750
- this.menuEl = menu2;
21944
+ });
20751
21945
  this.menuOwner = anchor;
20752
21946
  }
20753
21947
  closeMenu() {
20754
- if (this.menuEl) {
20755
- this.menuEl.remove();
20756
- this.menuEl = null;
20757
- }
20758
- this.menuOwner = null;
21948
+ this.menuPop?.hide();
20759
21949
  }
20760
21950
  /** A color control: an `icon` over a thin **colored underline** showing the current
20761
21951
  * value (the common idiom), with an invisible native picker overlaid to edit it.
@@ -20794,36 +21984,18 @@ var DrawingSettingsPopup = class {
20794
21984
  }
20795
21985
  /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
20796
21986
  openColorPopover(anchor, color, onChange) {
20797
- this.closeColorPopover();
20798
- this.closeMenu();
20799
21987
  const t = this.theme;
20800
- const pop = document.createElement("div");
20801
- pop.style.cssText = `position:absolute;z-index:25;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:10px;pointer-events:auto;`;
20802
- applyChromeTokens(pop, t);
20803
- pop.addEventListener("pointerdown", (e) => e.stopPropagation());
20804
- pop.appendChild(buildColorPicker(color, t, onChange));
20805
- this.host.appendChild(pop);
20806
- const ar = anchor.getBoundingClientRect();
20807
- const hr = this.host.getBoundingClientRect();
20808
- const w = pop.offsetWidth;
20809
- const h = pop.offsetHeight;
20810
- let left = ar.left - hr.left;
20811
- let top = ar.bottom - hr.top + 6;
20812
- if (left + w > hr.width - 4) left = hr.width - w - 4;
20813
- if (left < 4) left = 4;
20814
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
20815
- if (top < 4) top = 4;
20816
- pop.style.left = `${Math.round(left)}px`;
20817
- pop.style.top = `${Math.round(top)}px`;
20818
- this.colorPop = pop;
21988
+ this.colorPop = this.hostFloat(anchor, {
21989
+ zIndex: 25,
21990
+ padding: "10px",
21991
+ fill: (el) => {
21992
+ el.appendChild(buildColorPicker(color, t, onChange));
21993
+ }
21994
+ });
20819
21995
  this.colorOwner = anchor;
20820
21996
  }
20821
21997
  closeColorPopover() {
20822
- if (this.colorPop) {
20823
- this.colorPop.remove();
20824
- this.colorPop = null;
20825
- }
20826
- this.colorOwner = null;
21998
+ this.colorPop?.hide();
20827
21999
  }
20828
22000
  divider() {
20829
22001
  const d = document.createElement("div");
@@ -20962,7 +22134,7 @@ var DrawingToolbar = class {
20962
22134
  this.onCollapse = options.onCollapse ?? (() => {
20963
22135
  });
20964
22136
  this.onDrawingsSync = options.onDrawingsSync ?? null;
20965
- ensureStyles5();
22137
+ ensureStyles3();
20966
22138
  this.root = document.createElement("div");
20967
22139
  this.root.className = "vela-dtb";
20968
22140
  this.styleRoot();
@@ -21445,13 +22617,13 @@ var DrawingToolbar = class {
21445
22617
  }
21446
22618
  };
21447
22619
  var MAGNET_ID = "__magnet__";
21448
- var STYLE_ID5 = "vela-drawing-toolbar-styles";
21449
- function ensureStyles5() {
22620
+ var STYLE_ID4 = "vela-drawing-toolbar-styles";
22621
+ function ensureStyles3() {
21450
22622
  if (typeof document === "undefined") return;
21451
- let s = document.getElementById(STYLE_ID5);
22623
+ let s = document.getElementById(STYLE_ID4);
21452
22624
  if (!s) {
21453
22625
  s = document.createElement("style");
21454
- s.id = STYLE_ID5;
22626
+ s.id = STYLE_ID4;
21455
22627
  document.head.appendChild(s);
21456
22628
  }
21457
22629
  s.textContent = `
@@ -21830,9 +23002,9 @@ var UserDrawingController = class {
21830
23002
  }
21831
23003
  /**
21832
23004
  * Union of the visible drawings' price ranges on `paneId` whose time extent
21833
- * intersects [fromTime, toTime] folded into the pane's autoscale so an
21834
- * off-series drawing still expands the scale (mirrors Pine drawings). Hidden
21835
- * drawings and full-width references (hline null extent) are handled too.
23005
+ * intersects [fromTime, toTime]. Hidden drawings and full-width references
23006
+ * (hline null extent) are handled too. Kept as the seam for a future
23007
+ * per-drawing autoscale opt-in the renderer does not fold this in today.
21836
23008
  */
21837
23009
  priceRangeForPane(paneId, fromTime, toTime) {
21838
23010
  let lo = Infinity;
@@ -22707,7 +23879,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
22707
23879
 
22708
23880
  // src/renderers/native/chrome/AttributionMark.ts
22709
23881
  var ATTRIBUTION_URL = "https://luxalgo.com/vela";
22710
- var STYLE_ID6 = "vela-attribution-styles";
23882
+ var STYLE_ID5 = "vela-attribution-styles";
22711
23883
  var CSS2 = `
22712
23884
  .vela-attribution {
22713
23885
  text-decoration: none;
@@ -22770,11 +23942,11 @@ var CSS2 = `
22770
23942
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
22771
23943
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
22772
23944
  `;
22773
- function ensureStyles6(doc) {
22774
- let s = doc.getElementById(STYLE_ID6);
23945
+ function ensureStyles4(doc) {
23946
+ let s = doc.getElementById(STYLE_ID5);
22775
23947
  if (!s) {
22776
23948
  s = doc.createElement("style");
22777
- s.id = STYLE_ID6;
23949
+ s.id = STYLE_ID5;
22778
23950
  doc.head.appendChild(s);
22779
23951
  }
22780
23952
  s.textContent = CSS2;
@@ -22803,7 +23975,7 @@ function createCustomMark(doc, html, background) {
22803
23975
  return el;
22804
23976
  }
22805
23977
  function createAttributionMark(doc, background) {
22806
- ensureStyles6(doc);
23978
+ ensureStyles4(doc);
22807
23979
  const a = doc.createElement("a");
22808
23980
  a.className = "vela-attribution";
22809
23981
  a.href = ATTRIBUTION_URL;
@@ -25781,7 +26953,6 @@ var NativeRenderer = class {
25781
26953
  const n = this.coords.barCount;
25782
26954
  if (n === 0) return;
25783
26955
  const vr = this.coords.visibleLogicalRange();
25784
- const vtr = this.coords.visibleTimeRange();
25785
26956
  const i0 = Math.max(0, Math.floor(vr.from));
25786
26957
  const i1 = Math.min(n - 1, Math.ceil(vr.to));
25787
26958
  if (i1 < i0) return;
@@ -25799,9 +26970,7 @@ var NativeRenderer = class {
25799
26970
  }
25800
26971
  const models = this.scene.indicatorsForPane(pane.id);
25801
26972
  const masterModels = models.filter((m) => m.ownScale !== true);
25802
- let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
25803
- const udr = this.userDrawings?.priceRangeForPane(pane.id, vtr.from, vtr.to) ?? null;
25804
- if (udr) dr = dr ? { min: Math.min(dr.min, udr.min), max: Math.max(dr.max, udr.max) } : udr;
26973
+ const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
25805
26974
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
25806
26975
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
25807
26976
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -26798,55 +27967,6 @@ function resolveElement(container) {
26798
27967
  if (!element) throw new Error(`[vela] container not found for selector "${container}"`);
26799
27968
  return element;
26800
27969
  }
26801
-
26802
- // src/ui/styles.ts
26803
- function injectStyles(id, css, root = document) {
26804
- if (typeof document === "undefined") return;
26805
- const host = root instanceof Document ? root.head : root;
26806
- if (root.getElementById?.(id) ?? host.querySelector(`#${CSS.escape(id)}`)) return;
26807
- const s = document.createElement("style");
26808
- s.id = id;
26809
- s.textContent = css;
26810
- host.appendChild(s);
26811
- }
26812
-
26813
- // src/ui/tokens.ts
26814
- var STATIC_ID = "vela-ui-tokens";
26815
- var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
26816
- var STATIC_CSS = `
26817
- .vela-ui, .vela-ui-layer {
26818
- ${STATIC_DECLS}
26819
- font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
26820
- box-sizing: border-box;
26821
- }
26822
- .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
26823
- .vela-icon { display: inline-flex; align-items: center; flex: none; }
26824
- .vela-icon svg { display: block; }
26825
- `;
26826
- function applyThemeTokens(el, t) {
26827
- const tokens = themeTokens(t);
26828
- for (const key in tokens) el.style.setProperty(key, tokens[key]);
26829
- }
26830
- function applyPlotOverlayTokens(host, base, config) {
26831
- const layout = config?.layout;
26832
- const t = layout?.background && layout.textColor ? { ...base, background: layout.background, textColor: layout.textColor } : base;
26833
- applyThemeTokens(host, t);
26834
- }
26835
- function ensureUIHost(el, theme) {
26836
- injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
26837
- el.classList.add("vela-ui");
26838
- if (theme) applyThemeTokens(el, theme);
26839
- }
26840
-
26841
- // src/ui/icons.ts
26842
- function iconEl(id, doc = document) {
26843
- const span = doc.createElement("span");
26844
- span.className = "vela-icon";
26845
- span.setAttribute("aria-hidden", "true");
26846
- const svg = iconMarkup(id);
26847
- if (svg) span.innerHTML = svg;
26848
- return span;
26849
- }
26850
27970
  function runMachine(machine5, props, render) {
26851
27971
  const m = new vanilla.VanillaMachine(machine5, props);
26852
27972
  const unsub = m.subscribe(render);
@@ -27088,7 +28208,10 @@ function menuController(opts) {
27088
28208
  props: {
27089
28209
  id: opts.id ?? nextUid("vela-menu"),
27090
28210
  ids: opts.triggerId ? { trigger: opts.triggerId } : void 0,
27091
- positioning: { placement: opts.placement ?? "bottom-start" },
28211
+ positioning: {
28212
+ placement: opts.placement ?? "bottom-start",
28213
+ ...opts.getAnchorRect ? { getAnchorRect: () => opts.getAnchorRect() } : {}
28214
+ },
27092
28215
  onSelect: (d) => opts.onSelect?.(d.value),
27093
28216
  onOpenChange: (d) => opts.onOpenChange?.(d.open)
27094
28217
  },
@@ -27170,6 +28293,22 @@ var MENU_CSS = `
27170
28293
  }
27171
28294
  .vela-menu-switch.on { background: var(--vela-accent-bright, var(--vela-accent)); border-color: var(--vela-accent-bright, var(--vela-accent)); }
27172
28295
  .vela-menu-switch.on::after { transform: translateX(16px); background: var(--vela-bg); }
28296
+ /* Favorite star (rows carrying \`favorite\`): reserved space always \u2014 it fades in on row
28297
+ hover instead of shifting the layout \u2014 and a starred row keeps its filled star visible.
28298
+ Same control language as the drawing toolbar's flyout stars. */
28299
+ .vela-menu-item .vela-menu-star {
28300
+ order: 98;
28301
+ /* Pad the 16px icon slot into a bigger hit target without moving the row's layout. */
28302
+ margin: -3px -5px -3px 0;
28303
+ padding: 3px 5px;
28304
+ box-sizing: content-box;
28305
+ border-radius: var(--vela-radius-sm);
28306
+ opacity: 0;
28307
+ transition: opacity 0.1s ease, background 0.1s ease;
28308
+ }
28309
+ .vela-menu-item[data-highlighted] .vela-menu-star { opacity: 0.55; }
28310
+ .vela-menu-item .vela-menu-star:hover { opacity: 1; background: var(--vela-hover-strong); }
28311
+ .vela-menu-item .vela-menu-star.vela-fav { opacity: 0.95; color: var(--vela-highlight); }
27173
28312
  .vela-menu-sep { height: 1px; margin: 4px 6px; background: var(--vela-border); }
27174
28313
  /* Submenu trigger row: a right-aligned chevron, and it stays highlighted while its own
27175
28314
  list is open so the path you came down remains readable. */
@@ -27188,9 +28327,13 @@ var Surface = class _Surface {
27188
28327
  this.items = [];
27189
28328
  /** Branch item id → the surface it opens. */
27190
28329
  this.subs = /* @__PURE__ */ new Map();
28330
+ /** Trigger rect captured when this level opened — the list stays put if the
28331
+ * trigger then moves (favorite chips shifting the caret). */
28332
+ this.pinnedAnchor = null;
27191
28333
  this.doc = doc;
27192
28334
  this.host = opts.host;
27193
28335
  this.onSelect = opts.onSelect;
28336
+ this.onFavorite = opts.onFavorite;
27194
28337
  this.positioner = doc.createElement("div");
27195
28338
  this.positioner.className = "vela-ui-layer";
27196
28339
  this.list = doc.createElement("ul");
@@ -27198,16 +28341,25 @@ var Surface = class _Surface {
27198
28341
  if (opts.minWidth) this.list.style.minWidth = opts.minWidth;
27199
28342
  this.positioner.appendChild(this.list);
27200
28343
  this.host.appendChild(this.positioner);
28344
+ const trigger = opts.trigger;
27201
28345
  this.ctrl = menuController({
27202
28346
  items: [],
27203
28347
  id: opts.id,
27204
28348
  placement: opts.placement,
27205
28349
  onSelect: (id) => this.onSelect(id),
27206
- onOpenChange: opts.onOpenChange,
28350
+ getAnchorRect: () => this.pinnedAnchor,
28351
+ onOpenChange: (open2) => {
28352
+ if (open2 && trigger) {
28353
+ const r = trigger.getBoundingClientRect();
28354
+ this.pinnedAnchor = { x: r.x, y: r.y, width: r.width, height: r.height };
28355
+ } else {
28356
+ this.pinnedAnchor = null;
28357
+ }
28358
+ opts.onOpenChange?.(open2);
28359
+ },
27207
28360
  triggerId: opts.triggerId
27208
28361
  });
27209
28362
  this.mid = String(this.ctrl.props.id);
27210
- const trigger = opts.trigger;
27211
28363
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
27212
28364
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
27213
28365
  const api = this.ctrl.connect(service);
@@ -27287,6 +28439,25 @@ var Surface = class _Surface {
27287
28439
  hint.textContent = item.hint;
27288
28440
  li.appendChild(hint);
27289
28441
  }
28442
+ if (item.favorite !== void 0 && this.onFavorite) {
28443
+ const on = item.favorite;
28444
+ const star = iconEl(on ? "star-filled" : "star", doc);
28445
+ star.classList.add("vela-menu-star");
28446
+ if (on) star.classList.add("vela-fav");
28447
+ star.removeAttribute("aria-hidden");
28448
+ star.setAttribute("role", "button");
28449
+ star.setAttribute("aria-label", on ? "Remove from favorites" : "Add to favorites");
28450
+ star.setAttribute("aria-pressed", on ? "true" : "false");
28451
+ for (const ev of ["pointerdown", "pointerup", "mousedown", "mouseup"]) {
28452
+ star.addEventListener(ev, (e) => e.stopPropagation());
28453
+ }
28454
+ star.addEventListener("click", (e) => {
28455
+ e.stopPropagation();
28456
+ e.preventDefault();
28457
+ this.onFavorite?.(item.id, !on);
28458
+ });
28459
+ li.appendChild(star);
28460
+ }
27290
28461
  if (item.toggle) {
27291
28462
  const sw = doc.createElement("span");
27292
28463
  sw.className = "vela-menu-switch" + (item.checked ? " on" : "");
@@ -27302,6 +28473,7 @@ var Surface = class _Surface {
27302
28473
  host: this.host,
27303
28474
  placement: "right-start",
27304
28475
  onSelect: this.onSelect,
28476
+ onFavorite: this.onFavorite,
27305
28477
  id: `${this.mid}--${item.id}`
27306
28478
  });
27307
28479
  sub.setItems(item.submenu ?? []);
@@ -27325,7 +28497,8 @@ var Menu = class {
27325
28497
  trigger: opts.trigger,
27326
28498
  triggerId: opts.triggerId,
27327
28499
  id: opts.id,
27328
- minWidth: opts.minWidth
28500
+ minWidth: opts.minWidth,
28501
+ onFavorite: opts.onFavorite
27329
28502
  });
27330
28503
  this.root.setItems(opts.items);
27331
28504
  }
@@ -27829,7 +29002,7 @@ var Drawer = class {
27829
29002
  };
27830
29003
 
27831
29004
  // src/widget/layout-picker.ts
27832
- var STYLE_ID7 = "vela-widget-layout-picker-v14";
29005
+ var STYLE_ID6 = "vela-widget-layout-picker-v14";
27833
29006
  var CSS3 = `
27834
29007
  .vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
27835
29008
  .vela-lp {
@@ -27934,7 +29107,7 @@ var CSS3 = `
27934
29107
  `;
27935
29108
  var GRID = 4;
27936
29109
  function layoutGridCanvas(doc) {
27937
- injectStyles(STYLE_ID7, CSS3, doc);
29110
+ injectStyles(STYLE_ID6, CSS3, doc);
27938
29111
  const el = doc.createElement("div");
27939
29112
  el.className = "vela-lp-grid";
27940
29113
  const squares = [];
@@ -27974,7 +29147,7 @@ var LayoutPicker = class {
27974
29147
  };
27975
29148
  this.opts = opts;
27976
29149
  this.doc = opts.host.ownerDocument;
27977
- injectStyles(STYLE_ID7, CSS3, this.doc);
29150
+ injectStyles(STYLE_ID6, CSS3, this.doc);
27978
29151
  const doc = this.doc;
27979
29152
  this.layer = doc.createElement("div");
27980
29153
  this.layer.className = "vela-ui-layer vela-lp-layer";
@@ -28287,6 +29460,24 @@ function timeframeMs(value) {
28287
29460
  if (!Number.isFinite(count) || count < 1) return NaN;
28288
29461
  return count * UNIT_MS[key];
28289
29462
  }
29463
+ function favoriteTimeframeChips(favorites) {
29464
+ const seen = /* @__PURE__ */ new Set();
29465
+ const out = [];
29466
+ for (const tf of favorites) {
29467
+ if (seen.has(tf)) continue;
29468
+ seen.add(tf);
29469
+ out.push(tf);
29470
+ }
29471
+ return out.sort((a, b) => {
29472
+ const ma = timeframeMs(a);
29473
+ const mb = timeframeMs(b);
29474
+ const aOk = Number.isFinite(ma);
29475
+ const bOk = Number.isFinite(mb);
29476
+ if (aOk && bOk && ma !== mb) return ma - mb;
29477
+ if (aOk !== bOk) return aOk ? -1 : 1;
29478
+ return 0;
29479
+ });
29480
+ }
28290
29481
  function timeframeLabel(value) {
28291
29482
  const parsed = parseTimeframe(value);
28292
29483
  if (!parsed.valid) return value;
@@ -28301,7 +29492,7 @@ function timeframeLabel(value) {
28301
29492
  }
28302
29493
 
28303
29494
  // src/widget/topbar.ts
28304
- var STYLE_ID8 = "vela-topbar";
29495
+ var STYLE_ID7 = "vela-topbar";
28305
29496
  var CSS4 = `
28306
29497
  .vela-widget-topbar {
28307
29498
  display: flex;
@@ -28339,6 +29530,36 @@ var CSS4 = `
28339
29530
  color: var(--vela-fg-bright);
28340
29531
  }
28341
29532
  .vela-widget-symbol:hover, .vela-widget-tf:hover, .vela-widget-style:hover, .vela-widget-indicators:hover, .vela-widget-action-left:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
29533
+ /* Timeframe cluster: duration-sorted favorite chips, highlight in place, caret
29534
+ opening the full dropdown. With no favorites the caret is the merged trigger
29535
+ (label + chevron). An unstarred current value sits as an extra chip by the caret. */
29536
+ .vela-widget-tf-group { display: inline-flex; align-items: center; gap: 2px; }
29537
+ .vela-widget-tf-chips { display: inline-flex; align-items: center; gap: 2px; }
29538
+ .vela-widget-tf-chips:empty { display: none; }
29539
+ .vela-widget-tf[data-current='1'] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
29540
+ .vela-widget-tf-caret {
29541
+ all: unset;
29542
+ display: inline-flex;
29543
+ align-items: center;
29544
+ justify-content: center;
29545
+ width: 18px;
29546
+ height: 30px;
29547
+ border-radius: 4px;
29548
+ cursor: pointer;
29549
+ color: var(--vela-fg-muted);
29550
+ }
29551
+ .vela-widget-tf-caret:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
29552
+ .vela-widget-tf-caret[data-solo='1'] {
29553
+ width: auto;
29554
+ padding: 0 6px 0 9px;
29555
+ gap: 4px;
29556
+ color: var(--vela-fg-bright);
29557
+ background: var(--vela-hover-strong);
29558
+ font-size: 13px;
29559
+ font-weight: 550;
29560
+ white-space: nowrap;
29561
+ }
29562
+ .vela-widget-topbar .vela-widget-tf-caret .vela-icon { font-size: 14px; width: 14px; height: 14px; }
28342
29563
  .vela-widget-topbar .vela-icon { color: inherit; font-size: 16px; width: 16px; height: 16px; }
28343
29564
  /* Width is set in syncHairlines() to exactly one device pixel \u2014 a CSS 1px at
28344
29565
  fractional DPR (1.25, 1.5\u2026) straddles two physical pixels and siblings end
@@ -28436,16 +29657,23 @@ var Topbar = class {
28436
29657
  this.timeframe = opts.timeframe;
28437
29658
  this.priceStyle = opts.priceStyle;
28438
29659
  const doc = host.ownerDocument;
28439
- injectStyles(STYLE_ID8, CSS4, doc);
29660
+ injectStyles(STYLE_ID7, CSS4, doc);
28440
29661
  this.el = doc.createElement("div");
28441
29662
  this.el.className = "vela-widget-topbar";
28442
29663
  this.symbolEl = doc.createElement("button");
28443
29664
  this.symbolEl.className = "vela-widget-symbol";
28444
29665
  this.symbolEl.textContent = parseSymbol(opts.symbol).ticker;
28445
29666
  if (opts.onSymbolClick) this.symbolEl.addEventListener("click", opts.onSymbolClick);
28446
- this.tfButton = doc.createElement("button");
28447
- this.tfButton.className = "vela-widget-tf";
28448
- this.tfButton.textContent = timeframeLabel(this.timeframe);
29667
+ this.tfFavs = [...opts.timeframeFavorites ?? []];
29668
+ this.tfChipsHost = doc.createElement("span");
29669
+ this.tfChipsHost.className = "vela-widget-tf-chips";
29670
+ this.tfCaret = doc.createElement("button");
29671
+ this.tfCaret.className = "vela-widget-tf-caret";
29672
+ this.tfCaret.appendChild(iconEl("chevron-down", doc));
29673
+ this.tfCaret.setAttribute("aria-label", "Timeframes");
29674
+ const tfGroup = doc.createElement("span");
29675
+ tfGroup.className = "vela-widget-tf-group";
29676
+ tfGroup.append(this.tfChipsHost, this.tfCaret);
28449
29677
  this.styleButton = doc.createElement("button");
28450
29678
  this.styleButton.className = "vela-widget-style";
28451
29679
  this.renderStyleButton(doc);
@@ -28485,19 +29713,20 @@ var Topbar = class {
28485
29713
  d.className = "vela-sep";
28486
29714
  return d;
28487
29715
  };
28488
- const leading = [this.symbolEl, sep(), this.tfButton, sep(), this.styleButton, sep()];
29716
+ const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
28489
29717
  if (this.layoutButton) leading.push(this.layoutButton, sep());
28490
29718
  if (indicatorsBtn) leading.push(indicatorsBtn, sep());
28491
29719
  this.leftActionsSep = sep();
28492
29720
  this.leftActionsSep.hidden = true;
28493
29721
  this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
28494
29722
  host.appendChild(this.el);
29723
+ this.renderTfChips();
28495
29724
  this.onHairlineSync();
28496
29725
  this.hairlineRo = new ResizeObserver(this.onHairlineSync);
28497
29726
  this.hairlineRo.observe(this.el);
28498
29727
  doc.defaultView?.addEventListener("resize", this.onHairlineSync);
28499
29728
  this.renderActions();
28500
- this.tooltips.push(new Tooltip(this.tfButton, { content: "Timeframe", triggerId: "vela-topbar-tf", host }));
29729
+ this.tooltips.push(new Tooltip(this.tfCaret, { content: "Timeframe", triggerId: "vela-topbar-tf", host }));
28501
29730
  this.tooltips.push(new Tooltip(this.styleButton, { content: "Chart style", triggerId: "vela-topbar-style", host }));
28502
29731
  if (this.layoutButton && opts.layout) {
28503
29732
  this.tooltips.push(new Tooltip(this.layoutButton, { content: "Layout", triggerId: "vela-topbar-layout", host }));
@@ -28514,11 +29743,12 @@ var Topbar = class {
28514
29743
  });
28515
29744
  }
28516
29745
  this.tfMenu = new Menu({
28517
- trigger: this.tfButton,
29746
+ trigger: this.tfCaret,
28518
29747
  triggerId: "vela-topbar-tf",
28519
29748
  host,
28520
29749
  items: this.tfItems(),
28521
29750
  onSelect: (id) => opts.onTimeframe(id),
29751
+ onFavorite: (id, on) => opts.onTimeframeFavorite?.(id, on),
28522
29752
  // Timeframe labels are two-or-three characters ("1m", "4h", "1D") — the
28523
29753
  // stylesheet's default min-width would leave the list mostly empty.
28524
29754
  minWidth: "84px"
@@ -28536,8 +29766,48 @@ var Topbar = class {
28536
29766
  }
28537
29767
  setTimeframe(tf) {
28538
29768
  this.timeframe = tf;
28539
- this.tfButton.textContent = timeframeLabel(tf);
28540
29769
  this.tfMenu.setItems(this.tfItems());
29770
+ this.renderTfChips();
29771
+ }
29772
+ /** Reflect the favorite-timeframe set — the quick-switch chips and the dropdown stars. */
29773
+ setTimeframeFavorites(favs) {
29774
+ this.tfFavs = [...favs];
29775
+ this.renderTfChips();
29776
+ this.tfMenu.setItems(this.tfItems());
29777
+ }
29778
+ /** Rebuild the quick-switch chips (current value changed, or the favorite set did). */
29779
+ renderTfChips() {
29780
+ const doc = this.el.ownerDocument;
29781
+ this.tfChipsHost.replaceChildren();
29782
+ const stars = this.opts.onTimeframeFavorite !== void 0;
29783
+ const chips = stars ? favoriteTimeframeChips(this.tfFavs) : [];
29784
+ const currentIsFav = chips.includes(this.timeframe);
29785
+ const shown = currentIsFav || chips.length === 0 ? chips : [...chips, this.timeframe];
29786
+ for (const tf of shown) {
29787
+ const b = doc.createElement("button");
29788
+ b.className = "vela-widget-tf";
29789
+ const label = timeframeLabel(tf);
29790
+ b.textContent = label;
29791
+ if (tf === this.timeframe) {
29792
+ b.dataset.current = "1";
29793
+ b.setAttribute("aria-current", "true");
29794
+ } else {
29795
+ b.setAttribute("aria-label", `Switch timeframe to ${label}`);
29796
+ b.addEventListener("click", () => this.opts.onTimeframe(tf));
29797
+ }
29798
+ this.tfChipsHost.appendChild(b);
29799
+ }
29800
+ this.tfCaret.replaceChildren();
29801
+ if (chips.length === 0) {
29802
+ this.tfCaret.dataset.solo = "1";
29803
+ this.tfCaret.append(doc.createTextNode(timeframeLabel(this.timeframe)), iconEl("chevron-down", doc));
29804
+ this.tfCaret.setAttribute("aria-label", `Timeframe \u2014 ${timeframeLabel(this.timeframe)}`);
29805
+ } else {
29806
+ delete this.tfCaret.dataset.solo;
29807
+ this.tfCaret.appendChild(iconEl("chevron-down", doc));
29808
+ this.tfCaret.setAttribute("aria-label", "Timeframes");
29809
+ }
29810
+ this.onHairlineSync();
28541
29811
  }
28542
29812
  setPriceStyle(style) {
28543
29813
  this.priceStyle = style;
@@ -28615,9 +29885,9 @@ var Topbar = class {
28615
29885
  }
28616
29886
  /** Reflect a docked side panel's open state on its button — the panels toggle each other,
28617
29887
  * so the dock pushes the state rather than the button assuming it. */
28618
- setPanelActive(id, open) {
29888
+ setPanelActive(id, open2) {
28619
29889
  const btn = this.panelBtns.get(id);
28620
- if (btn) btn.dataset.active = open ? "1" : "";
29890
+ if (btn) btn.dataset.active = open2 ? "1" : "";
28621
29891
  }
28622
29892
  destroy() {
28623
29893
  this.hairlineRo?.disconnect();
@@ -28655,7 +29925,13 @@ var Topbar = class {
28655
29925
  }
28656
29926
  }
28657
29927
  tfItems() {
28658
- return this.opts.timeframes.map((tf) => ({ id: tf, label: timeframeLabel(tf), checked: tf === this.timeframe }));
29928
+ const stars = this.opts.onTimeframeFavorite !== void 0;
29929
+ return this.opts.timeframes.map((tf) => ({
29930
+ id: tf,
29931
+ label: timeframeLabel(tf),
29932
+ checked: tf === this.timeframe,
29933
+ ...stars ? { favorite: this.tfFavs.includes(tf) } : {}
29934
+ }));
28659
29935
  }
28660
29936
  styleItems() {
28661
29937
  return priceStyleIds().map((id, i) => ({
@@ -28678,10 +29954,10 @@ function fmtPrice(n, dp) {
28678
29954
  const d = dp ?? decimalsFor(n);
28679
29955
  return n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
28680
29956
  }
28681
- function fmtChange(open, close) {
28682
- if (open == null || close == null || !Number.isFinite(open) || !Number.isFinite(close) || open === 0) return "";
28683
- const diff = close - open;
28684
- const pct = diff / open * 100;
29957
+ function fmtChange(open2, close) {
29958
+ if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
29959
+ const diff = close - open2;
29960
+ const pct = diff / open2 * 100;
28685
29961
  const sign = diff >= 0 ? "+" : "";
28686
29962
  return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
28687
29963
  }
@@ -28730,7 +30006,7 @@ function tickerIconEl(doc, base, name, className) {
28730
30006
  }
28731
30007
 
28732
30008
  // src/widget/statusline.ts
28733
- var STYLE_ID9 = "vela-widget-statusline";
30009
+ var STYLE_ID8 = "vela-widget-statusline";
28734
30010
  var CSS5 = `
28735
30011
  .vela-statusline {
28736
30012
  position: absolute;
@@ -28922,7 +30198,7 @@ var Statusline = class {
28922
30198
  this.fitMode = false;
28923
30199
  this.fitRO = null;
28924
30200
  const doc = host.ownerDocument;
28925
- injectStyles(STYLE_ID9, CSS5, doc);
30201
+ injectStyles(STYLE_ID8, CSS5, doc);
28926
30202
  host.classList.add("vela-has-statusline");
28927
30203
  this.el = doc.createElement("div");
28928
30204
  this.el.className = "vela-statusline";
@@ -29097,7 +30373,7 @@ var Statusline = class {
29097
30373
  var MAX_FONT_PX = 36;
29098
30374
  var MIN_FONT_PX = 12;
29099
30375
  var FILL = 0.9;
29100
- var STYLE_ID10 = "vela-widget-watermark";
30376
+ var STYLE_ID9 = "vela-widget-watermark";
29101
30377
  var CSS6 = `
29102
30378
  .vela-watermark {
29103
30379
  position: absolute;
@@ -29136,7 +30412,7 @@ var Watermark = class {
29136
30412
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
29137
30413
  * fires during chart construction, before any subscriber can see it. */
29138
30414
  this.loading = true;
29139
- injectStyles(STYLE_ID10, CSS6, host.ownerDocument);
30415
+ injectStyles(STYLE_ID9, CSS6, host.ownerDocument);
29140
30416
  this.el = host.ownerDocument.createElement("div");
29141
30417
  this.el.className = "vela-watermark";
29142
30418
  this.el.dataset.velaScreenshot = "under";
@@ -29200,7 +30476,7 @@ var RANGE_PRESETS = [
29200
30476
  { id: "ALL", tf: "W", preset: "ALL", bars: 5e3 }
29201
30477
  // everything the provider serves
29202
30478
  ];
29203
- var STYLE_ID11 = "vela-widget-bottombar";
30479
+ var STYLE_ID10 = "vela-widget-bottombar";
29204
30480
  var CSS7 = `
29205
30481
  .vela-widget-bottombar {
29206
30482
  display: flex;
@@ -29276,7 +30552,7 @@ var Bottombar = class {
29276
30552
  this.timer = null;
29277
30553
  this.timezone = opts.timezone;
29278
30554
  const doc = host.ownerDocument;
29279
- injectStyles(STYLE_ID11, CSS7, doc);
30555
+ injectStyles(STYLE_ID10, CSS7, doc);
29280
30556
  this.el = doc.createElement("div");
29281
30557
  this.el.className = "vela-widget-bottombar";
29282
30558
  for (const preset of RANGE_PRESETS) {
@@ -29387,10 +30663,11 @@ function parseQuery(raw, venues) {
29387
30663
  function onlyOne(matches) {
29388
30664
  return matches.length === 1 ? matches[0] : null;
29389
30665
  }
30666
+ var venueOf = (s) => s.prefix ?? s.provider;
29390
30667
  function filterSymbols(list, query, limit = 100) {
29391
- const venues = [...new Set(list.map((s) => s.provider?.toLowerCase()).filter((p) => !!p))];
30668
+ const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
29392
30669
  const { scope, term } = parseQuery(query, venues);
29393
- const pool = scope ? list.filter((s) => s.provider?.toLowerCase() === scope) : list;
30670
+ const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
29394
30671
  const q = term.toUpperCase();
29395
30672
  if (!q) {
29396
30673
  if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
@@ -29409,12 +30686,12 @@ function filterSymbols(list, query, limit = 100) {
29409
30686
  if (t.startsWith(q)) prefix.push(s);
29410
30687
  else if (t.includes(q)) substr.push(s);
29411
30688
  else if ((s.description ?? "").toUpperCase().includes(q)) desc.push(s);
29412
- else if (!scope && s.provider?.toLowerCase().includes(qLower)) venue.push(s);
30689
+ else if (!scope && (venueOf(s)?.toLowerCase().includes(qLower) || s.provider?.toLowerCase().includes(qLower))) venue.push(s);
29413
30690
  if (prefix.length >= limit) break;
29414
30691
  }
29415
30692
  return [...prefix, ...substr, ...desc, ...venue].slice(0, limit);
29416
30693
  }
29417
- var STYLE_ID12 = "vela-widget-symbolpicker";
30694
+ var STYLE_ID11 = "vela-widget-symbolpicker";
29418
30695
  var CSS8 = `
29419
30696
  .vela-sp-searchrow {
29420
30697
  display: flex;
@@ -29512,7 +30789,7 @@ var SymbolPicker = class {
29512
30789
  this.seed = "";
29513
30790
  this.activeTab = "All";
29514
30791
  const doc = (opts.host ?? document.body).ownerDocument;
29515
- injectStyles(STYLE_ID12, CSS8, doc);
30792
+ injectStyles(STYLE_ID11, CSS8, doc);
29516
30793
  this.input = doc.createElement("input");
29517
30794
  this.input.className = "vela-sp-input";
29518
30795
  this.input.placeholder = "Search symbol\u2026";
@@ -29542,8 +30819,8 @@ var SymbolPicker = class {
29542
30819
  host: opts.host,
29543
30820
  closeOnInteractOutside: true,
29544
30821
  content: (body) => body.append(searchRow, this.tabs, this.list),
29545
- onOpenChange: (open) => {
29546
- if (open) {
30822
+ onOpenChange: (open2) => {
30823
+ if (open2) {
29547
30824
  this.input.value = this.seed;
29548
30825
  this.seed = "";
29549
30826
  this.refresh();
@@ -29552,7 +30829,7 @@ var SymbolPicker = class {
29552
30829
  this.input.setSelectionRange(this.input.value.length, this.input.value.length);
29553
30830
  }, 0);
29554
30831
  }
29555
- opts.onOpenChange?.(open);
30832
+ opts.onOpenChange?.(open2);
29556
30833
  }
29557
30834
  });
29558
30835
  this.input.addEventListener("input", () => this.refresh());
@@ -29561,14 +30838,14 @@ var SymbolPicker = class {
29561
30838
  else if (e.key === "ArrowUp") this.moveHighlight(-1);
29562
30839
  else if (e.key === "Enter") {
29563
30840
  const pick = this.rows[this.highlighted];
29564
- if (pick) this.select(pick.ticker, pick.provider, opts.onSelect);
30841
+ if (pick) this.select(pick.ticker, pick.prefix ?? pick.provider, opts.onSelect);
29565
30842
  return;
29566
30843
  } else return;
29567
30844
  e.preventDefault();
29568
30845
  });
29569
30846
  this.list.addEventListener("click", (e) => {
29570
30847
  const row = e.target.closest(".vela-sp-row");
29571
- if (row?.dataset.ticker) this.select(row.dataset.ticker, row.dataset.provider, opts.onSelect);
30848
+ if (row?.dataset.ticker) this.select(row.dataset.ticker, row.dataset.venue, opts.onSelect);
29572
30849
  });
29573
30850
  }
29574
30851
  /** Wire where symbols come from (re-called on every widget rebuild). */
@@ -29585,9 +30862,9 @@ var SymbolPicker = class {
29585
30862
  destroy() {
29586
30863
  this.dialog.destroy();
29587
30864
  }
29588
- select(ticker, provider, onSelect) {
30865
+ select(ticker, venue, onSelect) {
29589
30866
  this.close();
29590
- onSelect(provider ? `${provider.toLowerCase()}:${ticker}` : ticker);
30867
+ onSelect(venue ? `${venue}:${ticker}` : ticker);
29591
30868
  }
29592
30869
  moveHighlight(delta) {
29593
30870
  if (!this.rows.length) return;
@@ -29620,7 +30897,8 @@ var SymbolPicker = class {
29620
30897
  const row = doc.createElement("div");
29621
30898
  row.className = "vela-sp-row";
29622
30899
  row.dataset.ticker = s.ticker;
29623
- if (s.provider) row.dataset.provider = s.provider;
30900
+ const venue = s.prefix ?? s.provider;
30901
+ if (venue) row.dataset.venue = venue;
29624
30902
  const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
29625
30903
  const main = doc.createElement("span");
29626
30904
  main.className = "vela-sp-main";
@@ -29632,11 +30910,11 @@ var SymbolPicker = class {
29632
30910
  d.textContent = s.description ?? (s.type ?? "");
29633
30911
  main.append(t, d);
29634
30912
  row.append(av, main);
29635
- if (s.provider) {
30913
+ if (venue) {
29636
30914
  const badge = doc.createElement("span");
29637
30915
  badge.className = "vela-sp-badge";
29638
- badge.dataset.p = s.provider;
29639
- badge.textContent = s.provider;
30916
+ badge.dataset.p = s.provider ?? venue;
30917
+ badge.textContent = venue;
29640
30918
  row.appendChild(badge);
29641
30919
  }
29642
30920
  this.list.appendChild(row);
@@ -29834,7 +31112,7 @@ function treeIsEmpty(panes) {
29834
31112
  }
29835
31113
 
29836
31114
  // src/widget/side-panel.ts
29837
- var STYLE_ID13 = "vela-widget-sidepanel";
31115
+ var STYLE_ID12 = "vela-widget-sidepanel";
29838
31116
  var DEFAULT_PANEL_WIDTH = 280;
29839
31117
  var DEFAULT_PANEL_MIN_WIDTH = 200;
29840
31118
  var DEFAULT_PANEL_MAX_WIDTH = 640;
@@ -29938,7 +31216,7 @@ var SidePanel = class {
29938
31216
  * a programmatic {@link setWidth}, so restoring a persisted width raises no change. */
29939
31217
  this.onWidthChange = null;
29940
31218
  const doc = host.ownerDocument;
29941
- injectStyles(STYLE_ID13, CSS9, doc);
31219
+ injectStyles(STYLE_ID12, CSS9, doc);
29942
31220
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
29943
31221
  this.maxWidth = opts.resizable ? opts.maxWidth ?? DEFAULT_PANEL_MAX_WIDTH : Number.POSITIVE_INFINITY;
29944
31222
  this.declaredWidth = clampPanelWidth(opts.width ?? DEFAULT_PANEL_WIDTH, this.minWidth, this.maxWidth);
@@ -29970,10 +31248,10 @@ var SidePanel = class {
29970
31248
  return !this.el.hidden;
29971
31249
  }
29972
31250
  /** Open/close the panel — a bare call flips it. */
29973
- toggle(open = this.el.hidden) {
29974
- if (open === !this.el.hidden) return;
29975
- this.el.hidden = !open;
29976
- this.onOpenChange?.(open);
31251
+ toggle(open2 = this.el.hidden) {
31252
+ if (open2 === !this.el.hidden) return;
31253
+ this.el.hidden = !open2;
31254
+ this.onOpenChange?.(open2);
29977
31255
  }
29978
31256
  /** The scrolling body, for a panel filled from OUTSIDE the class — a contributed panel's
29979
31257
  * `mount` receives exactly this element. Subclasses use the protected `body`. */
@@ -30047,7 +31325,7 @@ var SidePanel = class {
30047
31325
  };
30048
31326
 
30049
31327
  // src/widget/object-tree.ts
30050
- var STYLE_ID14 = "vela-widget-objtree";
31328
+ var STYLE_ID13 = "vela-widget-objtree";
30051
31329
  var CSS10 = `
30052
31330
  /* One pane's block. The transparent border reserves the drop-target outline. */
30053
31331
  .vela-ot-pane {
@@ -30286,7 +31564,7 @@ var ObjectTree = class extends SidePanel {
30286
31564
  this.menu = null;
30287
31565
  this.menuActions = /* @__PURE__ */ new Map();
30288
31566
  this.drag = null;
30289
- injectStyles(STYLE_ID14, CSS10, host.ownerDocument);
31567
+ injectStyles(STYLE_ID13, CSS10, host.ownerDocument);
30290
31568
  this.body.addEventListener("contextmenu", (e) => this.onContextMenu(e));
30291
31569
  this.body.addEventListener("pointerdown", (e) => this.onPointerDown(e));
30292
31570
  }
@@ -30296,9 +31574,9 @@ var ObjectTree = class extends SidePanel {
30296
31574
  set groups(next) {
30297
31575
  if (this.chart) this.groupsPerChart.set(this.chart, next);
30298
31576
  }
30299
- toggle(open = this.el.hidden) {
30300
- super.toggle(open);
30301
- if (open) this.refresh();
31577
+ toggle(open2 = this.el.hidden) {
31578
+ super.toggle(open2);
31579
+ if (open2) this.refresh();
30302
31580
  }
30303
31581
  setSymbol(symbol) {
30304
31582
  this.symbolName = parseSymbol(symbol).ticker;
@@ -31268,7 +32546,7 @@ var ObjectTree = class extends SidePanel {
31268
32546
  };
31269
32547
 
31270
32548
  // src/widget/data-window.ts
31271
- var STYLE_ID15 = "vela-widget-datawindow";
32549
+ var STYLE_ID14 = "vela-widget-datawindow";
31272
32550
  var CSS11 = `
31273
32551
  .vela-dw-group {
31274
32552
  padding: 10px 8px 4px;
@@ -31328,11 +32606,11 @@ var DataWindow = class extends SidePanel {
31328
32606
  super(host, "Data window", "vela-dw");
31329
32607
  this.chart = null;
31330
32608
  this.unsubs = [];
31331
- injectStyles(STYLE_ID15, CSS11, host.ownerDocument);
32609
+ injectStyles(STYLE_ID14, CSS11, host.ownerDocument);
31332
32610
  }
31333
- toggle(open = this.el.hidden) {
31334
- super.toggle(open);
31335
- if (open) this.refresh();
32611
+ toggle(open2 = this.el.hidden) {
32612
+ super.toggle(open2);
32613
+ if (open2) this.refresh();
31336
32614
  }
31337
32615
  /** (Re)bind to a chart instance — called after every widget rebuild. */
31338
32616
  onChart(chart) {
@@ -31446,8 +32724,8 @@ var PanelDock = class {
31446
32724
  }
31447
32725
  }
31448
32726
  /** Open/close one panel by id — a bare call flips it. Unknown ids are ignored. */
31449
- toggle(id, open) {
31450
- this.entries.find((e) => e.id === id)?.panel.toggle(open);
32727
+ toggle(id, open2) {
32728
+ this.entries.find((e) => e.id === id)?.panel.toggle(open2);
31451
32729
  }
31452
32730
  /** The open panel's id, or null when the column is closed. */
31453
32731
  get openId() {
@@ -31461,8 +32739,8 @@ var PanelDock = class {
31461
32739
  /** The dock's persistable state, or null when there is nothing worth saving. */
31462
32740
  getState() {
31463
32741
  const out = {};
31464
- const open = this.openId;
31465
- if (open) out.open = open;
32742
+ const open2 = this.openId;
32743
+ if (open2) out.open = open2;
31466
32744
  if (this.widths.size > 0) out.widths = Object.fromEntries(this.widths);
31467
32745
  return out.open || out.widths ? out : null;
31468
32746
  }
@@ -31495,13 +32773,13 @@ var PanelDock = class {
31495
32773
  this.entries.sort((a, b) => a.order - b.order);
31496
32774
  const stored = this.widths.get(entry.id);
31497
32775
  if (stored !== void 0) entry.panel.setWidth(stored);
31498
- entry.panel.onOpenChange = (open) => {
31499
- if (open) {
32776
+ entry.panel.onOpenChange = (open2) => {
32777
+ if (open2) {
31500
32778
  for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
31501
32779
  this.pendingOpen = null;
31502
32780
  }
31503
- this.deps.chrome.setPanelActive(entry.id, open);
31504
- if (open) entry.handle?.onOpen?.();
32781
+ this.deps.chrome.setPanelActive(entry.id, open2);
32782
+ if (open2) entry.handle?.onOpen?.();
31505
32783
  this.deps.changed?.();
31506
32784
  };
31507
32785
  entry.panel.onWidthChange = (px) => {
@@ -31530,7 +32808,7 @@ var PanelDock = class {
31530
32808
  };
31531
32809
 
31532
32810
  // src/widget/shortcuts-help.ts
31533
- var STYLE_ID16 = "vela-widget-shortcuts";
32811
+ var STYLE_ID15 = "vela-widget-shortcuts";
31534
32812
  var CSS12 = `
31535
32813
  .vela-sh-cat {
31536
32814
  color: var(--vela-fg-muted);
@@ -31556,16 +32834,16 @@ var ShortcutsHelp = class {
31556
32834
  constructor(keymap, host, onOpenChange) {
31557
32835
  this.keymap = keymap;
31558
32836
  const doc = (host ?? document.body).ownerDocument;
31559
- injectStyles(STYLE_ID16, CSS12, doc);
32837
+ injectStyles(STYLE_ID15, CSS12, doc);
31560
32838
  this.list = doc.createElement("div");
31561
32839
  this.dialog = new Dialog({
31562
32840
  title: "Keyboard shortcuts",
31563
32841
  host,
31564
32842
  closeOnInteractOutside: true,
31565
32843
  content: (body) => body.appendChild(this.list),
31566
- onOpenChange: (open) => {
31567
- if (open) this.refresh();
31568
- onOpenChange?.(open);
32844
+ onOpenChange: (open2) => {
32845
+ if (open2) this.refresh();
32846
+ onOpenChange?.(open2);
31569
32847
  }
31570
32848
  });
31571
32849
  }
@@ -31844,7 +33122,7 @@ var ChartContextMenu = class {
31844
33122
  };
31845
33123
 
31846
33124
  // src/widget/indicator-picker.ts
31847
- var STYLE_ID17 = "vela-widget-indpicker";
33125
+ var STYLE_ID16 = "vela-widget-indpicker";
31848
33126
  var CSS13 = `
31849
33127
  .vela-ip-searchrow {
31850
33128
  display: flex;
@@ -31922,7 +33200,7 @@ var IndicatorPicker = class {
31922
33200
  this.isOpen = false;
31923
33201
  this.opts = opts;
31924
33202
  const doc = (opts.host ?? document.body).ownerDocument;
31925
- injectStyles(STYLE_ID17, CSS13, doc);
33203
+ injectStyles(STYLE_ID16, CSS13, doc);
31926
33204
  this.search = doc.createElement("input");
31927
33205
  this.search.className = "vela-ip-search";
31928
33206
  this.search.placeholder = "Search\u2026";
@@ -31940,14 +33218,14 @@ var IndicatorPicker = class {
31940
33218
  closeOnInteractOutside: true,
31941
33219
  // The picker adds/removes indicators live — keep the chart undimmed behind it.
31942
33220
  content: (body) => body.append(searchRow, this.list),
31943
- onOpenChange: (open) => {
31944
- this.isOpen = open;
31945
- if (open) {
33221
+ onOpenChange: (open2) => {
33222
+ this.isOpen = open2;
33223
+ if (open2) {
31946
33224
  this.search.value = "";
31947
33225
  this.refresh();
31948
33226
  if (!this.search.closest('[data-layout="mobile"]')) setTimeout(() => this.search.focus(), 0);
31949
33227
  }
31950
- opts.onOpenChange?.(open);
33228
+ opts.onOpenChange?.(open2);
31951
33229
  }
31952
33230
  });
31953
33231
  this.list.addEventListener("click", (e) => {
@@ -32050,7 +33328,7 @@ var IndicatorPicker = class {
32050
33328
  };
32051
33329
 
32052
33330
  // src/widget/timeframe-quick.ts
32053
- var STYLE_ID18 = "vela-widget-tfquick";
33331
+ var STYLE_ID17 = "vela-widget-tfquick";
32054
33332
  var CSS14 = `
32055
33333
  .vela-tq-input {
32056
33334
  display: block;
@@ -32074,7 +33352,7 @@ var CSS14 = `
32074
33352
  var TimeframeQuick = class {
32075
33353
  constructor(opts) {
32076
33354
  const doc = (opts.host ?? document.body).ownerDocument;
32077
- injectStyles(STYLE_ID18, CSS14, doc);
33355
+ injectStyles(STYLE_ID17, CSS14, doc);
32078
33356
  this.input = doc.createElement("input");
32079
33357
  this.input.className = "vela-tq-input";
32080
33358
  this.input.setAttribute("spellcheck", "false");
@@ -32086,7 +33364,7 @@ var TimeframeQuick = class {
32086
33364
  draggable: true,
32087
33365
  closeOnInteractOutside: true,
32088
33366
  content: (body) => body.append(this.input, this.hint),
32089
- onOpenChange: (open) => opts.onOpenChange?.(open)
33367
+ onOpenChange: (open2) => opts.onOpenChange?.(open2)
32090
33368
  });
32091
33369
  this.input.addEventListener("input", () => this.renderHint());
32092
33370
  this.input.addEventListener("keydown", (e) => {
@@ -32256,7 +33534,7 @@ var LayoutModeController = class {
32256
33534
  };
32257
33535
 
32258
33536
  // src/widget/mobile-bar.ts
32259
- var STYLE_ID19 = "vela-widget-mobilebar";
33537
+ var STYLE_ID18 = "vela-widget-mobilebar";
32260
33538
  var CSS15 = `
32261
33539
  .vela-mobilebar {
32262
33540
  display: none;
@@ -32307,7 +33585,7 @@ var MobileBar = class {
32307
33585
  constructor(host, opts) {
32308
33586
  this.opts = opts;
32309
33587
  const doc = host.ownerDocument;
32310
- injectStyles(STYLE_ID19, CSS15, doc);
33588
+ injectStyles(STYLE_ID18, CSS15, doc);
32311
33589
  this.el = doc.createElement("div");
32312
33590
  this.el.className = "vela-mobilebar";
32313
33591
  const item = (cls, label, onClick, icon2) => {
@@ -32366,7 +33644,7 @@ var MobileBar = class {
32366
33644
  };
32367
33645
 
32368
33646
  // src/widget/timeframe-drawer.ts
32369
- var STYLE_ID20 = "vela-widget-tf-drawer";
33647
+ var STYLE_ID19 = "vela-widget-tf-drawer";
32370
33648
  var CSS16 = `
32371
33649
  .vela-tfd-heading {
32372
33650
  padding: 4px 2px 8px;
@@ -32423,7 +33701,7 @@ var TimeframeDrawer = class {
32423
33701
  this.rangeChips = /* @__PURE__ */ new Map();
32424
33702
  this.tfChips = /* @__PURE__ */ new Map();
32425
33703
  const doc = opts.host.ownerDocument;
32426
- injectStyles(STYLE_ID20, CSS16, doc);
33704
+ injectStyles(STYLE_ID19, CSS16, doc);
32427
33705
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
32428
33706
  const rangeHeading = doc.createElement("div");
32429
33707
  rangeHeading.className = "vela-tfd-heading";
@@ -32481,7 +33759,7 @@ var TimeframeDrawer = class {
32481
33759
  };
32482
33760
 
32483
33761
  // src/widget/drawings-drawer.ts
32484
- var STYLE_ID21 = "vela-widget-drawings-drawer";
33762
+ var STYLE_ID20 = "vela-widget-drawings-drawer";
32485
33763
  var CSS17 = `
32486
33764
  /* Search + tabs stay pinned while the tool list scrolls underneath. */
32487
33765
  .vela-dd-sticky {
@@ -32584,7 +33862,7 @@ var DrawingsDrawer = class {
32584
33862
  this.definition = { groups: [] };
32585
33863
  this.activeGroup = "";
32586
33864
  const doc = opts.host.ownerDocument;
32587
- injectStyles(STYLE_ID21, CSS17, doc);
33865
+ injectStyles(STYLE_ID20, CSS17, doc);
32588
33866
  this.drawer = new Drawer({
32589
33867
  host: opts.host,
32590
33868
  title: "Drawings",
@@ -32726,7 +34004,7 @@ var DrawingsDrawer = class {
32726
34004
  };
32727
34005
 
32728
34006
  // src/widget/more-drawer.ts
32729
- var STYLE_ID22 = "vela-widget-more-drawer";
34007
+ var STYLE_ID21 = "vela-widget-more-drawer";
32730
34008
  var CSS18 = `
32731
34009
  .vela-md-actions {
32732
34010
  display: flex;
@@ -32802,7 +34080,7 @@ var MoreDrawer = class {
32802
34080
  constructor(opts) {
32803
34081
  this.opts = opts;
32804
34082
  this.view = "main";
32805
- injectStyles(STYLE_ID22, CSS18, opts.host.ownerDocument);
34083
+ injectStyles(STYLE_ID21, CSS18, opts.host.ownerDocument);
32806
34084
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
32807
34085
  }
32808
34086
  open() {
@@ -32994,7 +34272,7 @@ var MoreDrawer = class {
32994
34272
  };
32995
34273
 
32996
34274
  // src/widget/timezone-drawer.ts
32997
- var STYLE_ID23 = "vela-widget-timezone-drawer";
34275
+ var STYLE_ID22 = "vela-widget-timezone-drawer";
32998
34276
  var CSS19 = `
32999
34277
  .vela-tzd-list { padding: 2px 0 4px; }
33000
34278
  .vela-tzd-row {
@@ -33014,7 +34292,7 @@ var CSS19 = `
33014
34292
  var TimezoneDrawer = class {
33015
34293
  constructor(opts) {
33016
34294
  this.opts = opts;
33017
- injectStyles(STYLE_ID23, CSS19, opts.host.ownerDocument);
34295
+ injectStyles(STYLE_ID22, CSS19, opts.host.ownerDocument);
33018
34296
  this.drawer = new Drawer({ host: opts.host, title: "Time zone", onOpenChange: opts.onOpenChange });
33019
34297
  }
33020
34298
  open() {
@@ -33052,7 +34330,7 @@ var TimezoneDrawer = class {
33052
34330
  };
33053
34331
 
33054
34332
  // src/widget/price-scale-drawer.ts
33055
- var STYLE_ID24 = "vela-widget-price-scale-drawer";
34333
+ var STYLE_ID23 = "vela-widget-price-scale-drawer";
33056
34334
  var CSS20 = `
33057
34335
  .vela-psd-list { padding: 2px 0 4px; }
33058
34336
  .vela-psd-row {
@@ -33082,7 +34360,7 @@ var PriceScaleDrawer = class {
33082
34360
  constructor(opts) {
33083
34361
  this.opts = opts;
33084
34362
  this.pane = null;
33085
- injectStyles(STYLE_ID24, CSS20, opts.host.ownerDocument);
34363
+ injectStyles(STYLE_ID23, CSS20, opts.host.ownerDocument);
33086
34364
  this.drawer = new Drawer({ host: opts.host, title: "Price scale", onOpenChange: opts.onOpenChange });
33087
34365
  }
33088
34366
  open() {
@@ -33218,7 +34496,7 @@ var PriceScaleDrawer = class {
33218
34496
  };
33219
34497
 
33220
34498
  // src/widget/drawing-pill.ts
33221
- var STYLE_ID25 = "vela-widget-drawing-pill";
34499
+ var STYLE_ID24 = "vela-widget-drawing-pill";
33222
34500
  var CSS21 = `
33223
34501
  .vela-drawpill { display: none; }
33224
34502
  [data-layout='mobile'] .vela-drawpill {
@@ -33279,7 +34557,7 @@ var DrawingPill = class {
33279
34557
  this.chart = null;
33280
34558
  this.chartSubs = [];
33281
34559
  const doc = host.ownerDocument;
33282
- injectStyles(STYLE_ID25, CSS21, doc);
34560
+ injectStyles(STYLE_ID24, CSS21, doc);
33283
34561
  this.el = doc.createElement("div");
33284
34562
  this.el.className = "vela-drawpill";
33285
34563
  this.el.hidden = true;
@@ -33388,6 +34666,10 @@ function sanitizeState(doc) {
33388
34666
  const favs = d.favorites.filter((f) => typeof f === "string");
33389
34667
  if (favs.length > 0) out.favorites = favs;
33390
34668
  }
34669
+ if (Array.isArray(d.timeframeFavorites)) {
34670
+ const favs = d.timeframeFavorites.filter((f) => typeof f === "string");
34671
+ if (favs.length > 0) out.timeframeFavorites = favs;
34672
+ }
33391
34673
  const sync = sanitizeSync(d.sync);
33392
34674
  if (sync) out.sync = sync;
33393
34675
  const tracks = sanitizeTrackSizes(d.trackSizes);
@@ -33661,7 +34943,7 @@ var WidgetHistory = class {
33661
34943
  };
33662
34944
 
33663
34945
  // src/widget/toast.ts
33664
- var STYLE_ID26 = "vela-widget-toast";
34946
+ var STYLE_ID25 = "vela-widget-toast";
33665
34947
  var CSS22 = `
33666
34948
  .vela-toast {
33667
34949
  position: absolute;
@@ -33692,7 +34974,7 @@ var CSS22 = `
33692
34974
  var Toast = class {
33693
34975
  constructor(host) {
33694
34976
  this.timer = null;
33695
- injectStyles(STYLE_ID26, CSS22, host.ownerDocument);
34977
+ injectStyles(STYLE_ID25, CSS22, host.ownerDocument);
33696
34978
  this.el = host.ownerDocument.createElement("div");
33697
34979
  this.el.className = "vela-toast";
33698
34980
  host.appendChild(this.el);
@@ -33828,6 +35110,8 @@ var VelaWidget = class {
33828
35110
  this.stateListeners = /* @__PURE__ */ new Set();
33829
35111
  /** Favorite drawing-tool types — mirrored from chart events, reapplied on rebuilds. */
33830
35112
  this.favs = [];
35113
+ /** Favorite timeframes — the topbar's quick-switch chips, persisted with the shell. */
35114
+ this.tfFavs = [];
33831
35115
  /** Mounted attachment disposers (by id), torn down at destroy. */
33832
35116
  this.attachmentDisposers = /* @__PURE__ */ new Map();
33833
35117
  this.onUnload = () => this.persistNow();
@@ -33870,6 +35154,7 @@ var VelaWidget = class {
33870
35154
  this.indicatorTitlesOn = bootCell?.indicatorTitles ?? true;
33871
35155
  this.indicatorValuesOn = bootCell?.indicatorValues ?? true;
33872
35156
  this.favs = boot?.favorites ? [...boot.favorites] : [];
35157
+ this.tfFavs = boot?.timeframeFavorites ? [...boot.timeframeFavorites] : [];
33873
35158
  this.savedConfig = bootCell?.rendererConfig ?? null;
33874
35159
  this.savedDrawings = bootCell?.drawings ?? null;
33875
35160
  this.pendingIndicators = bootCell?.indicators ?? null;
@@ -33883,9 +35168,9 @@ var VelaWidget = class {
33883
35168
  this.symbolPicker = new SymbolPicker({
33884
35169
  host: this.root,
33885
35170
  onSelect: (symbol) => this.setSymbol(symbol),
33886
- onOpenChange: (open) => {
33887
- if (open) this.inner?.renderer.closeDialogs();
33888
- this.trackDialog(open);
35171
+ onOpenChange: (open2) => {
35172
+ if (open2) this.inner?.renderer.closeDialogs();
35173
+ this.trackDialog(open2);
33889
35174
  }
33890
35175
  });
33891
35176
  this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
@@ -33908,15 +35193,15 @@ var VelaWidget = class {
33908
35193
  if (i < present.length) this.removeNative(present[i].type);
33909
35194
  else this.removeInstance(i - present.length);
33910
35195
  },
33911
- onOpenChange: (open) => {
33912
- if (open) this.inner?.renderer.closeDialogs();
33913
- this.trackDialog(open);
35196
+ onOpenChange: (open2) => {
35197
+ if (open2) this.inner?.renderer.closeDialogs();
35198
+ this.trackDialog(open2);
33914
35199
  }
33915
35200
  }) : null;
33916
35201
  this.tfQuick = new TimeframeQuick({
33917
35202
  host: this.root,
33918
35203
  onApply: (tf) => this.setTimeframe(tf),
33919
- onOpenChange: (open) => this.trackDialog(open)
35204
+ onOpenChange: (open2) => this.trackDialog(open2)
33920
35205
  });
33921
35206
  const picker = this.indicatorPicker;
33922
35207
  this.topbar = new Topbar(this.root, {
@@ -33929,8 +35214,10 @@ var VelaWidget = class {
33929
35214
  onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
33930
35215
  timeframe: this.timeframe,
33931
35216
  timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
35217
+ timeframeFavorites: this.tfFavs,
33932
35218
  priceStyle: this.priceStyle,
33933
35219
  onTimeframe: (tf) => this.setTimeframe(tf),
35220
+ onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
33934
35221
  onPriceStyle: (style) => this.setPriceStyle(style),
33935
35222
  getContext: () => this.context()
33936
35223
  });
@@ -34041,7 +35328,7 @@ var VelaWidget = class {
34041
35328
  label: "Show this shortcuts panel",
34042
35329
  category: "Help",
34043
35330
  run: () => {
34044
- this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open) => this.trackDialog(open)));
35331
+ this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open2) => this.trackDialog(open2)));
34045
35332
  this.shortcutsHelp.open();
34046
35333
  }
34047
35334
  });
@@ -34075,7 +35362,7 @@ var VelaWidget = class {
34075
35362
  activeRange: () => this.activeRangeId,
34076
35363
  onTimeframe: (tf) => this.setTimeframe(tf),
34077
35364
  onRange: (preset) => this.applyRange(preset),
34078
- onOpenChange: (open) => this.trackDialog(open)
35365
+ onOpenChange: (open2) => this.trackDialog(open2)
34079
35366
  }));
34080
35367
  this.tfDrawer.open();
34081
35368
  }
@@ -34087,7 +35374,7 @@ var VelaWidget = class {
34087
35374
  isFavorite: (type) => this.inner?.drawings.isFavorite(type) ?? false,
34088
35375
  onFavorite: (type, on) => this.inner?.drawings.setFavorite(type, on),
34089
35376
  onSelect: (type) => this.inner?.drawings.setTool(type),
34090
- onOpenChange: (open) => this.trackDialog(open)
35377
+ onOpenChange: (open2) => this.trackDialog(open2)
34091
35378
  }));
34092
35379
  this.drawingsDrawer.open();
34093
35380
  }
@@ -34108,7 +35395,7 @@ var VelaWidget = class {
34108
35395
  // Left-aligned actions have their own bottom-bar stop — only the rest
34109
35396
  // lands in the drawer, or every left action would appear twice.
34110
35397
  actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
34111
- onOpenChange: (open) => this.trackDialog(open)
35398
+ onOpenChange: (open2) => this.trackDialog(open2)
34112
35399
  }));
34113
35400
  this.moreDrawer.open();
34114
35401
  }
@@ -34117,7 +35404,7 @@ var VelaWidget = class {
34117
35404
  host: this.root,
34118
35405
  timezone: () => this.timezone,
34119
35406
  onTimezone: (zone) => this.setTimezone(zone),
34120
- onOpenChange: (open) => this.trackDialog(open)
35407
+ onOpenChange: (open2) => this.trackDialog(open2)
34121
35408
  }));
34122
35409
  this.timezoneDrawer.open();
34123
35410
  }
@@ -34127,7 +35414,7 @@ var VelaWidget = class {
34127
35414
  host: this.root,
34128
35415
  chart: () => this.inner,
34129
35416
  pressY: () => this.priceScalePressY,
34130
- onOpenChange: (open) => this.trackDialog(open)
35417
+ onOpenChange: (open2) => this.trackDialog(open2)
34131
35418
  }));
34132
35419
  this.priceScaleDrawer.open();
34133
35420
  }
@@ -34156,7 +35443,7 @@ var VelaWidget = class {
34156
35443
  setTimeframe: (tf) => this.setTimeframe(tf),
34157
35444
  setPriceStyle: (style) => this.setPriceStyle(style),
34158
35445
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
34159
- togglePanel: (id, open) => this.dock.toggle(id, open),
35446
+ togglePanel: (id, open2) => this.dock.toggle(id, open2),
34160
35447
  host: this.root,
34161
35448
  toast: (message, kind) => this.toast?.show(message, kind)
34162
35449
  };
@@ -34198,13 +35485,15 @@ var VelaWidget = class {
34198
35485
  this.statusline?.setMeta(tf, this.providerLabel());
34199
35486
  }
34200
35487
  /**
34201
- * The provider name to show: the one that RESOLVED the symbol, not the one configured.
34202
- * They differ whenever the `provider` option is just a default (or names something that
34203
- * isn't registered) the status line must not claim a venue that served nothing.
35488
+ * The venue label to show: the LISTING prefix the resolved symbol's data declares
35489
+ * (`NASDAQ` for AAPL), else the provider that RESOLVED the symbol never the one
35490
+ * configured or typed. They differ whenever the `provider` option is just a default,
35491
+ * the prefix was a listing venue, or the spelling isn't registered — the status line
35492
+ * must not claim a venue that served nothing.
34204
35493
  */
34205
35494
  providerLabel() {
34206
- const resolved = this.inner?.data.resolve(this.symbol)?.provider;
34207
- return resolved ?? parseSymbol(this.symbol).provider ?? "";
35495
+ const display = this.inner?.data.displayPrefix(this.symbol);
35496
+ return display ?? parseSymbol(this.symbol).provider ?? "";
34208
35497
  }
34209
35498
  setTimeframe(tf) {
34210
35499
  if (tf === this.timeframe || this.destroyed) return;
@@ -34215,6 +35504,14 @@ var VelaWidget = class {
34215
35504
  this.markStateDirty();
34216
35505
  void this.inner?.setMarket({ timeframe: tf, bars: this.bars });
34217
35506
  }
35507
+ /** Star/unstar a timeframe — the topbar chips and dropdown stars follow, and the
35508
+ * set persists with the rest of the shell state. */
35509
+ setTimeframeFavorite(tf, on) {
35510
+ if (on === this.tfFavs.includes(tf)) return;
35511
+ this.tfFavs = on ? [tf, ...this.tfFavs] : this.tfFavs.filter((f) => f !== tf);
35512
+ this.topbar.setTimeframeFavorites(this.tfFavs);
35513
+ this.markStateDirty();
35514
+ }
34218
35515
  /**
34219
35516
  * Switch the chart symbol in place. An `EXCHANGE:` prefix pins the venue (the
34220
35517
  * symbol picker composes one from the row the user pointed at) — a bare ticker
@@ -34272,6 +35569,7 @@ var VelaWidget = class {
34272
35569
  });
34273
35570
  const state = { version: 1, layout: "1", activeCellId: "c1", timezone: this.timezone, charts: [{ id: "c1", ...cell }] };
34274
35571
  if (this.favs.length > 0) state.favorites = [...this.favs];
35572
+ if (this.tfFavs.length > 0) state.timeframeFavorites = [...this.tfFavs];
34275
35573
  const panels2 = this.dock.getState();
34276
35574
  if (panels2) state.panels = panels2;
34277
35575
  return state;
@@ -34295,6 +35593,10 @@ var VelaWidget = class {
34295
35593
  this.favs = [...st.favorites];
34296
35594
  this.inner?.drawings.setFavorites(this.favs);
34297
35595
  }
35596
+ if (st.timeframeFavorites) {
35597
+ this.tfFavs = [...st.timeframeFavorites];
35598
+ this.topbar.setTimeframeFavorites(this.tfFavs);
35599
+ }
34298
35600
  this.dock.applyState(st.panels);
34299
35601
  if (cell) {
34300
35602
  const style = fromUrl.priceStyle ?? cell.priceStyle;
@@ -34551,6 +35853,9 @@ var VelaWidget = class {
34551
35853
  for (const [language, make] of Object.entries(resolveEngines(engines))) chart.registerEngine(language, make());
34552
35854
  this.inner = chart;
34553
35855
  this.symbolPicker.setSource(() => chart.data.symbols());
35856
+ void chart.data.ready().then(() => {
35857
+ if (this.inner === chart) this.statusline?.setMeta(this.timeframe, this.providerLabel());
35858
+ });
34554
35859
  if (this.layoutCtl !== void 0) chart.renderer.setLayoutMode(this.layoutCtl.current);
34555
35860
  this.objectTree.setSymbol(this.symbol);
34556
35861
  chart.renderer.setLegendActions(legendActionsProviderFor(chart, () => this.context()));
@@ -34869,9 +36174,9 @@ var VelaWidget = class {
34869
36174
  this.tfQuick.open(key);
34870
36175
  }
34871
36176
  }
34872
- trackDialog(open) {
34873
- this.openDialogs = Math.max(0, this.openDialogs + (open ? 1 : -1));
34874
- if (open) this.keymap.pushScope("dialog");
36177
+ trackDialog(open2) {
36178
+ this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));
36179
+ if (open2) this.keymap.pushScope("dialog");
34875
36180
  else this.keymap.popScope("dialog");
34876
36181
  }
34877
36182
  downloadScreenshot() {