@luxalgo/vela 0.5.2 → 0.5.4

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-DTOEXKTI.d.ts → DataProvider-Cqc_BaJ9.d.ts} +1 -1
  3. package/dist/{DataProvider-gnzv-dgc.d.cts → DataProvider-DgS2UyMc.d.cts} +1 -1
  4. package/dist/{chunk-RRFGWZNJ.js → chunk-D6WM5IUE.js} +786 -864
  5. package/dist/{chunk-LJLZR44Y.js → chunk-EMS6PO2T.js} +5 -7
  6. package/dist/{chunk-Y7TK4ZWK.js → chunk-JBQUY2RI.js} +126 -19
  7. package/dist/chunk-OGRQW3M6.js +1328 -0
  8. package/dist/{chunk-P556H6EA.js → chunk-QWIEKZRE.js} +59 -34
  9. package/dist/{chunk-B5TIKQJ5.js → chunk-ZNL2X6U2.js} +16 -2
  10. package/dist/{contributions-Ci_i3IN2.d.cts → contributions-4Nh1jbvb.d.cts} +2 -2
  11. package/dist/{contributions-b6AVrqqG.d.ts → contributions-DuIxJWeH.d.ts} +2 -2
  12. package/dist/{history-Cf1lVsap.d.cts → history-FsmvCIo4.d.ts} +5 -3
  13. package/dist/{history-Ci3M0xLV.d.ts → history-HofqYEh2.d.cts} +5 -3
  14. package/dist/index.cjs +2783 -1611
  15. package/dist/index.d.cts +14 -8
  16. package/dist/index.d.ts +14 -8
  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-DfH4Y-Om.d.cts → plugin-DJR6z3e0.d.cts} +8 -7
  21. package/dist/{plugin-CnNd7cuF.d.ts → plugin-N_EaXN4-.d.ts} +8 -7
  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 +2777 -1605
  37. package/dist/vela.global.min.js +315 -66
  38. package/dist/widget.cjs +2358 -1052
  39. package/dist/widget.d.cts +26 -8
  40. package/dist/widget.d.ts +26 -8
  41. package/dist/widget.js +27 -9
  42. package/dist/workspace.cjs +2264 -946
  43. package/dist/workspace.d.cts +10 -5
  44. package/dist/workspace.d.ts +10 -5
  45. package/dist/workspace.js +25 -7
  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(
@@ -9675,12 +9679,6 @@ function mix(base, top, alpha) {
9675
9679
  const c = (x, y) => Math.round(x + (y - x) * alpha);
9676
9680
  return `rgb(${c(b.r, t.r)},${c(b.g, t.g)},${c(b.b, t.b)})`;
9677
9681
  }
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
9682
  function isDarkColor(color) {
9685
9683
  const c = parseRgb(color);
9686
9684
  if (!c) return true;
@@ -9699,6 +9697,9 @@ var STATIC_TOKENS = {
9699
9697
  "--vela-z-tooltip": "60",
9700
9698
  "--vela-z-menu": "50",
9701
9699
  "--vela-z-dialog": "40",
9700
+ // Form popovers portal to <body> (or a chart host) and must sit above a dialog
9701
+ // whose own stacking context may be nested inside the chart container.
9702
+ "--vela-z-popover": "6000",
9702
9703
  "--vela-ease": "cubic-bezier(0.22, 1, 0.36, 1)",
9703
9704
  "--vela-dur-fast": "90ms",
9704
9705
  "--vela-dur-med": "160ms",
@@ -9881,6 +9882,1246 @@ function makeDialogDraggable(box, header, options) {
9881
9882
  return { reset };
9882
9883
  }
9883
9884
 
9885
+ // src/ui/styles.ts
9886
+ function injectStyles(id, css, root = document) {
9887
+ if (typeof document === "undefined") return;
9888
+ const host = root instanceof Document ? root.head : root;
9889
+ if (root.getElementById?.(id) ?? host.querySelector(`#${CSS.escape(id)}`)) return;
9890
+ const s = document.createElement("style");
9891
+ s.id = id;
9892
+ s.textContent = css;
9893
+ host.appendChild(s);
9894
+ }
9895
+
9896
+ // src/ui/icons.ts
9897
+ function iconEl(id, doc = document) {
9898
+ const span = doc.createElement("span");
9899
+ span.className = "vela-icon";
9900
+ span.setAttribute("aria-hidden", "true");
9901
+ const svg = iconMarkup(id);
9902
+ if (svg) span.innerHTML = svg;
9903
+ return span;
9904
+ }
9905
+
9906
+ // src/ui/components/switch/controller.ts
9907
+ function switchController(opts = {}) {
9908
+ let checked = opts.checked ?? false;
9909
+ const disabled = opts.disabled ?? false;
9910
+ return {
9911
+ get checked() {
9912
+ return checked;
9913
+ },
9914
+ disabled,
9915
+ size: opts.size ?? "md",
9916
+ tone: opts.tone ?? "bright",
9917
+ setChecked(v) {
9918
+ checked = v;
9919
+ },
9920
+ toggle() {
9921
+ if (disabled) return checked;
9922
+ checked = !checked;
9923
+ opts.onChange?.(checked);
9924
+ return checked;
9925
+ }
9926
+ };
9927
+ }
9928
+
9929
+ // src/ui/components/switch/styles.ts
9930
+ var SWITCH_STYLE_ID = "vela-ui-switch";
9931
+ var SWITCH_CSS = `
9932
+ .vela-switch {
9933
+ width: 20px;
9934
+ height: 20px;
9935
+ flex: 0 0 auto;
9936
+ display: inline-flex;
9937
+ align-items: center;
9938
+ justify-content: center;
9939
+ padding: 0;
9940
+ border: 1px solid var(--vela-border);
9941
+ border-radius: 5px;
9942
+ background: transparent;
9943
+ color: transparent;
9944
+ cursor: pointer;
9945
+ line-height: 0;
9946
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
9947
+ }
9948
+ .vela-switch .vela-icon, .vela-switch svg { display: block; width: 12px; height: 12px; }
9949
+ .vela-switch[data-size='sm'] .vela-icon, .vela-switch[data-size='sm'] svg { width: 11px; height: 11px; }
9950
+ .vela-switch:hover { border-color: var(--vela-fg-muted); }
9951
+ .vela-switch[data-checked] {
9952
+ background: var(--vela-fg-bright);
9953
+ border-color: var(--vela-fg-bright);
9954
+ color: var(--vela-bg);
9955
+ }
9956
+ .vela-switch[data-size='sm'] { width: 18px; height: 18px; border-color: var(--vela-border-strong); }
9957
+ .vela-switch[data-size='sm']:hover { border-color: var(--vela-fg-muted); }
9958
+ .vela-switch[data-size='sm'][data-checked] {
9959
+ background: var(--vela-selected-bg);
9960
+ border-color: var(--vela-selected-bg);
9961
+ color: var(--vela-selected-fg);
9962
+ }
9963
+ .vela-switch[data-tone='selected'][data-checked] {
9964
+ background: var(--vela-selected-bg);
9965
+ border-color: var(--vela-selected-bg);
9966
+ color: var(--vela-selected-fg);
9967
+ }
9968
+ .vela-switch[disabled] { opacity: 0.4; cursor: default; }
9969
+ `;
9970
+
9971
+ // src/ui/components/switch/view.ts
9972
+ var Switch = class {
9973
+ constructor(opts = {}) {
9974
+ injectStyles(SWITCH_STYLE_ID, SWITCH_CSS, document);
9975
+ this.ctrl = switchController(opts);
9976
+ const b = document.createElement("button");
9977
+ b.type = "button";
9978
+ if (opts.id) b.id = opts.id;
9979
+ b.className = "vela-switch";
9980
+ b.dataset.size = this.ctrl.size;
9981
+ b.dataset.tone = this.ctrl.tone;
9982
+ b.setAttribute("role", "switch");
9983
+ if (this.ctrl.disabled) b.disabled = true;
9984
+ b.appendChild(iconEl("check", b.ownerDocument));
9985
+ this.el = b;
9986
+ this.paint();
9987
+ b.addEventListener("pointerdown", (e) => {
9988
+ e.preventDefault();
9989
+ b.focus({ preventScroll: true });
9990
+ });
9991
+ b.addEventListener("click", () => {
9992
+ this.ctrl.toggle();
9993
+ this.paint();
9994
+ });
9995
+ }
9996
+ get checked() {
9997
+ return this.ctrl.checked;
9998
+ }
9999
+ setChecked(v) {
10000
+ this.ctrl.setChecked(v);
10001
+ this.paint();
10002
+ }
10003
+ paint() {
10004
+ const on = this.ctrl.checked;
10005
+ this.el.setAttribute("aria-checked", on ? "true" : "false");
10006
+ if (on) this.el.dataset.checked = "";
10007
+ else delete this.el.dataset.checked;
10008
+ }
10009
+ };
10010
+
10011
+ // src/ui/tokens.ts
10012
+ var STATIC_ID = "vela-ui-tokens";
10013
+ var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
10014
+ var STATIC_CSS = `
10015
+ .vela-ui, .vela-ui-layer {
10016
+ ${STATIC_DECLS}
10017
+ font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
10018
+ box-sizing: border-box;
10019
+ }
10020
+ .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
10021
+ .vela-icon { display: inline-flex; align-items: center; flex: none; }
10022
+ .vela-icon svg { display: block; }
10023
+ `;
10024
+ function applyThemeTokens(el, t) {
10025
+ const tokens = themeTokens(t);
10026
+ for (const key in tokens) el.style.setProperty(key, tokens[key]);
10027
+ }
10028
+ function applyPlotOverlayTokens(host, base, config) {
10029
+ const layout = config?.layout;
10030
+ const t = layout?.background && layout.textColor ? { ...base, background: layout.background, textColor: layout.textColor } : base;
10031
+ applyThemeTokens(host, t);
10032
+ }
10033
+ function ensureUIHost(el, theme) {
10034
+ injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
10035
+ el.classList.add("vela-ui");
10036
+ if (theme) applyThemeTokens(el, theme);
10037
+ }
10038
+
10039
+ // src/ui/components/popover/controller.ts
10040
+ function insetRect(r, inset) {
10041
+ return {
10042
+ left: r.left + inset,
10043
+ top: r.top + inset,
10044
+ right: r.right - inset,
10045
+ bottom: r.bottom - inset,
10046
+ width: r.width - inset * 2,
10047
+ height: r.height - inset * 2
10048
+ };
10049
+ }
10050
+ function viewportRect(width, height, inset) {
10051
+ return {
10052
+ left: inset,
10053
+ top: inset,
10054
+ right: width - inset,
10055
+ bottom: height - inset,
10056
+ width: width - inset * 2,
10057
+ height: height - inset * 2
10058
+ };
10059
+ }
10060
+ function intersectRects(a, b) {
10061
+ const left = Math.max(a.left, b.left);
10062
+ const top = Math.max(a.top, b.top);
10063
+ const right = Math.min(a.right, b.right);
10064
+ const bottom = Math.min(a.bottom, b.bottom);
10065
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
10066
+ }
10067
+ function placePopover(a) {
10068
+ let left = a.align === "end" ? a.trigger.right - a.pop.width : a.trigger.left;
10069
+ const below = a.trigger.bottom + a.gap;
10070
+ const above = a.trigger.top - a.pop.height - a.gap;
10071
+ const fitsBelow = below + a.pop.height <= a.clamp.bottom;
10072
+ const fitsAbove = above >= a.clamp.top;
10073
+ let top = below;
10074
+ if (!fitsBelow && (fitsAbove || a.trigger.top - a.clamp.top > a.clamp.bottom - a.trigger.bottom)) {
10075
+ top = Math.max(a.clamp.top, above);
10076
+ }
10077
+ if (left + a.pop.width > a.clamp.right) left = a.clamp.right - a.pop.width;
10078
+ if (left < a.clamp.left) left = a.clamp.left;
10079
+ if (top + a.pop.height > a.clamp.bottom) top = a.clamp.bottom - a.pop.height;
10080
+ if (top < a.clamp.top) top = a.clamp.top;
10081
+ return {
10082
+ left: Math.round(left - a.originX),
10083
+ top: Math.round(top - a.originY)
10084
+ };
10085
+ }
10086
+ function popoverController(opts = {}) {
10087
+ return {
10088
+ gap: opts.gap ?? 4,
10089
+ align: opts.align ?? "start",
10090
+ matchWidth: opts.matchWidth ?? false,
10091
+ position: opts.position ?? "fixed",
10092
+ boundaryInset: opts.boundaryInset ?? 0,
10093
+ viewportInset: opts.viewportInset ?? 6,
10094
+ onClose: opts.onClose
10095
+ };
10096
+ }
10097
+
10098
+ // src/ui/components/popover/styles.ts
10099
+ var POPOVER_STYLE_ID = "vela-ui-popover";
10100
+ var POPOVER_CSS = `
10101
+ .vela-popover {
10102
+ position: fixed;
10103
+ z-index: var(--vela-z-popover);
10104
+ box-sizing: border-box;
10105
+ /* Never serve as a scroll anchor: the shell is portaled and re-placed between
10106
+ gestures, and anchoring against it jumps the scroller underneath. */
10107
+ overflow-anchor: none;
10108
+ }
10109
+ .vela-popover[data-position='absolute'] { position: absolute; }
10110
+ .vela-popover[hidden] { display: none !important; }
10111
+ `;
10112
+
10113
+ // src/ui/components/popover/view.ts
10114
+ var open = null;
10115
+ function closeOpenPopovers() {
10116
+ open?.hide();
10117
+ }
10118
+ function isPopoverOpen() {
10119
+ return open !== null;
10120
+ }
10121
+ function openPopoverTrigger() {
10122
+ return open?.trigger ?? null;
10123
+ }
10124
+ function toRect(r) {
10125
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height };
10126
+ }
10127
+ var Popover = class {
10128
+ constructor(opts) {
10129
+ this.onOutside = null;
10130
+ this.onKey = null;
10131
+ this.onReflow = null;
10132
+ this.shown = false;
10133
+ const doc = opts.trigger.ownerDocument;
10134
+ injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
10135
+ this.trigger = opts.trigger;
10136
+ this.host = opts.host ?? doc.body;
10137
+ this.ctrl = popoverController(opts);
10138
+ this.boundary = opts.boundary ?? "viewport";
10139
+ this.theme = opts.theme;
10140
+ this.el = doc.createElement("div");
10141
+ this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
10142
+ this.el.dataset.position = this.ctrl.position;
10143
+ if (opts.zIndex !== void 0) this.el.style.zIndex = String(opts.zIndex);
10144
+ this.el.addEventListener("pointerdown", (e) => e.stopPropagation());
10145
+ if (opts.content instanceof Node) this.el.appendChild(opts.content);
10146
+ else if (typeof opts.content === "function") opts.content(this.el);
10147
+ }
10148
+ get open() {
10149
+ return this.shown;
10150
+ }
10151
+ get position() {
10152
+ return this.ctrl.position;
10153
+ }
10154
+ get align() {
10155
+ return this.ctrl.align;
10156
+ }
10157
+ show() {
10158
+ if (this.shown) {
10159
+ this.place();
10160
+ return;
10161
+ }
10162
+ if (open && open !== this) open.hide();
10163
+ ensureUIHost(this.el, this.theme);
10164
+ this.host.appendChild(this.el);
10165
+ this.shown = true;
10166
+ open = this;
10167
+ this.place();
10168
+ const onOutside = (ev) => {
10169
+ const t = ev.target;
10170
+ if (this.el.contains(t) || this.trigger.contains(t)) return;
10171
+ this.hide();
10172
+ };
10173
+ const onKey = (ev) => {
10174
+ if (ev.key !== "Escape") return;
10175
+ ev.preventDefault();
10176
+ ev.stopPropagation();
10177
+ this.hide();
10178
+ };
10179
+ const onReflow = () => this.place();
10180
+ setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
10181
+ document.addEventListener("keydown", onKey, true);
10182
+ window.addEventListener("resize", onReflow, true);
10183
+ document.addEventListener("scroll", onReflow, true);
10184
+ this.onOutside = onOutside;
10185
+ this.onKey = onKey;
10186
+ this.onReflow = onReflow;
10187
+ }
10188
+ hide() {
10189
+ if (!this.shown) return;
10190
+ if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
10191
+ if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
10192
+ if (this.onReflow) {
10193
+ window.removeEventListener("resize", this.onReflow, true);
10194
+ document.removeEventListener("scroll", this.onReflow, true);
10195
+ }
10196
+ this.onOutside = null;
10197
+ this.onKey = null;
10198
+ this.onReflow = null;
10199
+ this.el.remove();
10200
+ this.shown = false;
10201
+ if (open === this) open = null;
10202
+ this.ctrl.onClose?.();
10203
+ }
10204
+ /** Show if closed, hide if this instance is the open popover. */
10205
+ toggle() {
10206
+ if (this.shown) this.hide();
10207
+ else this.show();
10208
+ }
10209
+ destroy() {
10210
+ this.hide();
10211
+ }
10212
+ reposition() {
10213
+ if (this.shown) this.place();
10214
+ }
10215
+ clampRect() {
10216
+ const view = viewportRect(window.innerWidth, window.innerHeight, this.ctrl.viewportInset);
10217
+ const bound = this.readBoundary();
10218
+ if (!bound) return view;
10219
+ const inset = insetRect(bound, this.ctrl.boundaryInset);
10220
+ return intersectRects(view, inset);
10221
+ }
10222
+ readBoundary() {
10223
+ const b = this.boundary;
10224
+ if (b === "viewport") return null;
10225
+ const raw = typeof b === "function" ? b() : b.getBoundingClientRect();
10226
+ return raw ? toRect(raw) : null;
10227
+ }
10228
+ place() {
10229
+ const ar = this.trigger.getBoundingClientRect();
10230
+ if (this.ctrl.matchWidth) {
10231
+ this.el.style.minWidth = `${Math.round(Math.max(this.el.offsetWidth, ar.width))}px`;
10232
+ }
10233
+ const origin = this.ctrl.position === "absolute" ? this.host.getBoundingClientRect() : { left: 0, top: 0 };
10234
+ const pos = placePopover({
10235
+ trigger: toRect(ar),
10236
+ pop: { width: this.el.offsetWidth, height: this.el.offsetHeight },
10237
+ gap: this.ctrl.gap,
10238
+ align: this.ctrl.align,
10239
+ clamp: this.clampRect(),
10240
+ originX: origin.left,
10241
+ originY: origin.top
10242
+ });
10243
+ this.el.style.left = `${pos.left}px`;
10244
+ this.el.style.top = `${pos.top}px`;
10245
+ }
10246
+ };
10247
+
10248
+ // src/ui/components/select/controller.ts
10249
+ function selectController(opts) {
10250
+ const options = opts.options;
10251
+ let value = opts.value ?? options[0]?.value ?? "";
10252
+ const size = opts.size ?? "md";
10253
+ return {
10254
+ options,
10255
+ get value() {
10256
+ return value;
10257
+ },
10258
+ size,
10259
+ fill: opts.fill ?? size !== "sm",
10260
+ disabled: opts.disabled ?? false,
10261
+ setValue(v) {
10262
+ value = v;
10263
+ },
10264
+ labelOf(v) {
10265
+ return options.find((o) => o.value === v)?.label ?? v;
10266
+ },
10267
+ pick(v) {
10268
+ value = v;
10269
+ const label = options.find((o) => o.value === v)?.label ?? v;
10270
+ opts.onChange?.(v, label);
10271
+ return { value: v, label };
10272
+ }
10273
+ };
10274
+ }
10275
+
10276
+ // src/ui/components/select/styles.ts
10277
+ var SELECT_STYLE_ID = "vela-ui-select";
10278
+ var SELECT_CSS = `
10279
+ .vela-select { position: relative; display: inline-block; min-width: 0; }
10280
+ .vela-select[data-fill] { width: 100%; }
10281
+ .vela-select-trigger {
10282
+ display: flex;
10283
+ align-items: center;
10284
+ width: 100%;
10285
+ box-sizing: border-box;
10286
+ height: 34px;
10287
+ padding: 0 26px 0 8px;
10288
+ background: transparent;
10289
+ border: 1px solid var(--vela-border-strong);
10290
+ border-radius: 6px;
10291
+ color: var(--vela-fg-bright);
10292
+ font-size: 14px;
10293
+ font-family: inherit;
10294
+ cursor: pointer;
10295
+ text-align: left;
10296
+ outline: none;
10297
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
10298
+ }
10299
+ .vela-select-trigger:hover { border-color: var(--vela-fg-muted); }
10300
+ .vela-select-trigger:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
10301
+ /* As a grid item (chart-settings rows dissolve via display:contents into a shared
10302
+ max-content column) the wrap must NOT stretch to the column: it sizes to its own
10303
+ widest option, like the native select it replaces. */
10304
+ .vela-select[data-size='sm'] { max-width: 200px; flex: 0 0 auto; justify-self: start; }
10305
+ .vela-select[data-size='sm'] .vela-select-trigger {
10306
+ height: 28px;
10307
+ background: var(--vela-surface-elev);
10308
+ border-radius: var(--vela-radius-sm);
10309
+ color: var(--vela-fg);
10310
+ font-size: 13px;
10311
+ box-shadow: none;
10312
+ }
10313
+ .vela-select[data-size='sm'] .vela-select-trigger:focus { box-shadow: none; border-color: var(--vela-fg-muted); }
10314
+ .vela-select-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
10315
+ /* Invisible option-label stack: gives the shrink-to-fit wrap the WIDEST option's
10316
+ intrinsic width (like a native select), so picking a shorter/longer label never
10317
+ resizes the trigger and shifts the row around it. */
10318
+ .vela-select-sizer { visibility: hidden; height: 0; overflow: hidden; font-size: 14px; }
10319
+ .vela-select[data-size='sm'] .vela-select-sizer { font-size: 13px; }
10320
+ .vela-select-sizer span { display: block; height: 0; white-space: nowrap; padding: 0 26px 0 8px; border-inline: 1px solid transparent; }
10321
+ .vela-select-chevron {
10322
+ position: absolute;
10323
+ right: 8px;
10324
+ top: 50%;
10325
+ transform: translateY(-50%);
10326
+ pointer-events: none;
10327
+ display: flex;
10328
+ opacity: 0.55;
10329
+ line-height: 0;
10330
+ color: inherit;
10331
+ }
10332
+ .vela-select-chevron .vela-icon, .vela-select-chevron svg { width: 12px; height: 12px; display: block; }
10333
+ .vela-select-list {
10334
+ background: var(--vela-bg);
10335
+ color: var(--vela-fg);
10336
+ border: none;
10337
+ border-radius: 6px;
10338
+ box-shadow: var(--vela-shadow);
10339
+ font: 14px var(--vela-font);
10340
+ padding: 4px;
10341
+ overflow: hidden;
10342
+ }
10343
+ .vela-select-list[data-size='sm'] { font-size: 13px; background: var(--vela-surface-overlay); }
10344
+ .vela-select-items { max-height: none; overflow: hidden; }
10345
+ .vela-select-list.is-scroll { display: flex; align-items: stretch; gap: 2px; }
10346
+ .vela-select-list.is-scroll .vela-select-items {
10347
+ flex: 1 1 auto;
10348
+ min-width: 0;
10349
+ max-height: 240px;
10350
+ overflow-y: auto;
10351
+ scrollbar-width: none;
10352
+ }
10353
+ .vela-select-list.is-scroll .vela-select-items::-webkit-scrollbar { display: none; width: 0; height: 0; }
10354
+ .vela-select-rail { position: relative; flex: none; width: 3px; margin: 2px 1px 2px 0; pointer-events: none; }
10355
+ .vela-select-thumb { width: 3px; border-radius: 2px; background: var(--vela-scroll); }
10356
+ .vela-select-item {
10357
+ display: block;
10358
+ width: 100%;
10359
+ text-align: left;
10360
+ padding: 7px 10px;
10361
+ border: none;
10362
+ border-radius: 4px;
10363
+ background: transparent;
10364
+ color: inherit;
10365
+ cursor: pointer;
10366
+ font: inherit;
10367
+ font-weight: 400;
10368
+ }
10369
+ .vela-select-item:hover { background: var(--vela-hover); }
10370
+ .vela-select-item[data-checked] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
10371
+ .vela-select-item[data-checked]:hover { background: var(--vela-hover-strong); }
10372
+ `;
10373
+
10374
+ // src/ui/components/select/view.ts
10375
+ function ensureSelectStyles(doc) {
10376
+ injectStyles(SELECT_STYLE_ID, SELECT_CSS, doc);
10377
+ }
10378
+ function syncThumb(list, thumb) {
10379
+ const view = list.clientHeight;
10380
+ const total = list.scrollHeight;
10381
+ if (total <= view) {
10382
+ thumb.style.height = "0";
10383
+ return;
10384
+ }
10385
+ const thumbH = Math.max(20, view / total * view);
10386
+ const top = list.scrollTop / (total - view) * (view - thumbH);
10387
+ thumb.style.height = `${Math.round(thumbH)}px`;
10388
+ thumb.style.transform = `translateY(${Math.round(top)}px`;
10389
+ }
10390
+ function fillSelectList(menu2, options, current, onPick) {
10391
+ menu2.replaceChildren();
10392
+ const list = menu2.ownerDocument.createElement("div");
10393
+ list.className = "vela-select-items";
10394
+ for (const p of options) {
10395
+ const item = menu2.ownerDocument.createElement("button");
10396
+ item.type = "button";
10397
+ item.className = "vela-select-item";
10398
+ item.textContent = p.label;
10399
+ if (p.value === current) item.dataset.checked = "1";
10400
+ item.addEventListener("click", (e) => {
10401
+ e.stopPropagation();
10402
+ onPick(p.value, p.label);
10403
+ });
10404
+ list.appendChild(item);
10405
+ }
10406
+ menu2.appendChild(list);
10407
+ }
10408
+ function decorateSelectScroll(menu2) {
10409
+ const list = menu2.querySelector(".vela-select-items");
10410
+ if (!list) return;
10411
+ if (list.scrollHeight > 240) {
10412
+ menu2.classList.add("is-scroll");
10413
+ const rail = menu2.ownerDocument.createElement("div");
10414
+ rail.className = "vela-select-rail";
10415
+ const thumb = menu2.ownerDocument.createElement("div");
10416
+ thumb.className = "vela-select-thumb";
10417
+ rail.appendChild(thumb);
10418
+ menu2.appendChild(rail);
10419
+ list.addEventListener("scroll", () => syncThumb(list, thumb));
10420
+ syncThumb(list, thumb);
10421
+ }
10422
+ menu2.querySelector(".vela-select-item[data-checked]")?.scrollIntoView({ block: "nearest" });
10423
+ list.dispatchEvent(new Event("scroll"));
10424
+ }
10425
+ function toggleSelectList(trigger, options, current, onPick, opts = {}) {
10426
+ if (openPopoverTrigger() === trigger) {
10427
+ closeOpenPopovers();
10428
+ return null;
10429
+ }
10430
+ return openSelectList(trigger, options, current, onPick, opts);
10431
+ }
10432
+ function openSelectList(trigger, options, current, onPick, opts = {}) {
10433
+ ensureSelectStyles(trigger.ownerDocument);
10434
+ closeOpenPopovers();
10435
+ const pop = new Popover({
10436
+ trigger,
10437
+ theme: opts.theme,
10438
+ matchWidth: opts.matchWidth ?? true,
10439
+ boundary: opts.boundary,
10440
+ boundaryInset: opts.boundaryInset,
10441
+ gap: opts.gap ?? 4,
10442
+ align: opts.align ?? "start",
10443
+ host: opts.host,
10444
+ position: opts.position,
10445
+ zIndex: opts.zIndex,
10446
+ className: "vela-select-list",
10447
+ onClose: opts.onClose,
10448
+ content: (el) => {
10449
+ if (opts.size) el.dataset.size = opts.size;
10450
+ fillSelectList(el, options, current, (value, label) => {
10451
+ pop.hide();
10452
+ onPick(value, label);
10453
+ });
10454
+ }
10455
+ });
10456
+ pop.show();
10457
+ decorateSelectScroll(pop.el);
10458
+ pop.reposition();
10459
+ return pop;
10460
+ }
10461
+ var Select = class {
10462
+ constructor(opts) {
10463
+ this.list = null;
10464
+ const doc = opts.list?.host?.ownerDocument ?? document;
10465
+ ensureSelectStyles(doc);
10466
+ this.ctrl = selectController(opts);
10467
+ this.listOpts = { ...opts.list, theme: opts.theme ?? opts.list?.theme, size: this.ctrl.size };
10468
+ const wrap = doc.createElement("div");
10469
+ wrap.className = "vela-select";
10470
+ wrap.dataset.size = this.ctrl.size;
10471
+ if (this.ctrl.fill) wrap.dataset.fill = "";
10472
+ const btn = doc.createElement("button");
10473
+ btn.type = "button";
10474
+ if (opts.id) btn.id = opts.id;
10475
+ btn.className = "vela-select-trigger";
10476
+ if (this.ctrl.disabled) btn.disabled = true;
10477
+ const label = doc.createElement("span");
10478
+ label.className = "vela-select-label";
10479
+ label.textContent = this.ctrl.labelOf(this.ctrl.value);
10480
+ const chevron = doc.createElement("span");
10481
+ chevron.className = "vela-select-chevron";
10482
+ chevron.appendChild(iconEl("chevron-down", doc));
10483
+ btn.append(label, chevron);
10484
+ wrap.appendChild(btn);
10485
+ const sizer = doc.createElement("div");
10486
+ sizer.className = "vela-select-sizer";
10487
+ sizer.setAttribute("aria-hidden", "true");
10488
+ for (const o of this.ctrl.options) {
10489
+ const s = doc.createElement("span");
10490
+ s.textContent = o.label;
10491
+ sizer.appendChild(s);
10492
+ }
10493
+ wrap.appendChild(sizer);
10494
+ btn.addEventListener("pointerdown", (e) => {
10495
+ e.preventDefault();
10496
+ btn.focus({ preventScroll: true });
10497
+ });
10498
+ btn.addEventListener("click", (e) => {
10499
+ e.stopPropagation();
10500
+ this.toggle();
10501
+ });
10502
+ this.el = wrap;
10503
+ this.trigger = btn;
10504
+ this.labelEl = label;
10505
+ }
10506
+ get value() {
10507
+ return this.ctrl.value;
10508
+ }
10509
+ setValue(v) {
10510
+ this.ctrl.setValue(v);
10511
+ this.labelEl.textContent = this.ctrl.labelOf(v);
10512
+ }
10513
+ toggle() {
10514
+ this.list = toggleSelectList(
10515
+ this.trigger,
10516
+ this.ctrl.options,
10517
+ this.ctrl.value,
10518
+ (value, label) => {
10519
+ this.ctrl.pick(value);
10520
+ this.labelEl.textContent = label;
10521
+ this.list = null;
10522
+ },
10523
+ { ...this.listOpts, onClose: () => {
10524
+ this.list = null;
10525
+ this.listOpts.onClose?.();
10526
+ } }
10527
+ );
10528
+ }
10529
+ destroy() {
10530
+ this.list?.hide();
10531
+ this.list = null;
10532
+ }
10533
+ };
10534
+
10535
+ // src/ui/components/number-input/controller.ts
10536
+ function clampNumber(n, opts) {
10537
+ let v = n;
10538
+ if (opts.min !== void 0) v = Math.max(opts.min, v);
10539
+ if (opts.max !== void 0) v = Math.min(opts.max, v);
10540
+ if (opts.integer) v = Math.round(v);
10541
+ return v;
10542
+ }
10543
+ function decimalsOf(n) {
10544
+ const s = String(n);
10545
+ const e = s.search(/[eE]/);
10546
+ if (e >= 0) {
10547
+ const frac = s.slice(0, e).split(".")[1]?.length ?? 0;
10548
+ return Math.max(0, Math.min(20, frac - Number(s.slice(e + 1))));
10549
+ }
10550
+ return s.split(".")[1]?.length ?? 0;
10551
+ }
10552
+ function snapToStep(n, base, step) {
10553
+ const d = Math.min(20, Math.max(decimalsOf(base), decimalsOf(step)));
10554
+ return Number(n.toFixed(d));
10555
+ }
10556
+ function numberInputController(opts) {
10557
+ let value = opts.value;
10558
+ const integer = opts.integer ?? false;
10559
+ const commit = opts.commit ?? "blur";
10560
+ const clamp = opts.clamp ?? commit === "blur";
10561
+ const step = opts.step ?? (integer ? 1 : 0.1);
10562
+ const onChange = opts.onChange;
10563
+ const normalize = (raw) => clamp ? clampNumber(raw, { min: opts.min, max: opts.max, integer }) : raw;
10564
+ const apply = (raw) => {
10565
+ if (!Number.isFinite(raw)) return null;
10566
+ const n = normalize(raw);
10567
+ if (n !== value) {
10568
+ value = n;
10569
+ onChange?.(n);
10570
+ }
10571
+ return n;
10572
+ };
10573
+ return {
10574
+ get value() {
10575
+ return value;
10576
+ },
10577
+ min: opts.min,
10578
+ max: opts.max,
10579
+ step,
10580
+ integer,
10581
+ size: opts.size ?? "md",
10582
+ commit,
10583
+ steppers: opts.steppers ?? commit === "blur",
10584
+ clamp,
10585
+ disabled: opts.disabled ?? false,
10586
+ apply,
10587
+ sync(raw) {
10588
+ const n = Number.isFinite(raw) ? normalize(raw) : value;
10589
+ value = n;
10590
+ return n;
10591
+ },
10592
+ nudge(dir) {
10593
+ return apply(snapToStep(value + dir * step, value, step));
10594
+ }
10595
+ };
10596
+ }
10597
+
10598
+ // src/ui/components/number-input/styles.ts
10599
+ var NUMBER_STYLE_ID = "vela-ui-number";
10600
+ var NUMBER_CSS = `
10601
+ .vela-num { position: relative; display: inline-block; min-width: 0; }
10602
+ .vela-num[data-fill] { width: 100%; }
10603
+ .vela-num input {
10604
+ width: 100%;
10605
+ box-sizing: border-box;
10606
+ height: 34px;
10607
+ background: transparent;
10608
+ border: 1px solid var(--vela-border-strong);
10609
+ border-radius: 6px;
10610
+ color: var(--vela-fg-bright);
10611
+ padding: 0 8px;
10612
+ font-size: 14px;
10613
+ font-family: inherit;
10614
+ outline: none;
10615
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
10616
+ -moz-appearance: textfield;
10617
+ appearance: textfield;
10618
+ }
10619
+ .vela-num input::-webkit-inner-spin-button, .vela-num input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
10620
+ .vela-num input:hover { border-color: var(--vela-fg-muted); }
10621
+ .vela-num input:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
10622
+ .vela-num[data-steppers] input { padding-right: 26px; }
10623
+ .vela-num[data-size='sm'] { width: 64px; flex: none; }
10624
+ .vela-num[data-size='sm'][data-compact] { width: 56px; }
10625
+ .vela-num[data-size='sm'] input {
10626
+ height: 28px;
10627
+ background: var(--vela-surface-elev);
10628
+ border-radius: var(--vela-radius-sm);
10629
+ color: var(--vela-fg);
10630
+ font-size: 13px;
10631
+ box-shadow: none;
10632
+ }
10633
+ .vela-num[data-size='sm'] input:focus { box-shadow: none; }
10634
+ .vela-num-step {
10635
+ position: absolute;
10636
+ right: 0;
10637
+ top: 0;
10638
+ bottom: 0;
10639
+ width: 22px;
10640
+ display: none;
10641
+ flex-direction: column;
10642
+ justify-content: center;
10643
+ box-sizing: border-box;
10644
+ padding: 2px 6px 2px 0;
10645
+ line-height: 0;
10646
+ }
10647
+ .vela-num[data-steppers]:hover .vela-num-step { display: flex; }
10648
+ .vela-num-step button {
10649
+ flex: 1;
10650
+ width: 100%;
10651
+ min-height: 0;
10652
+ border: none;
10653
+ background: transparent;
10654
+ color: var(--vela-fg-muted);
10655
+ border-radius: 3px;
10656
+ padding: 0;
10657
+ cursor: pointer;
10658
+ display: flex;
10659
+ align-items: center;
10660
+ justify-content: center;
10661
+ }
10662
+ .vela-num-step button:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
10663
+ .vela-num-step .vela-icon, .vela-num-step svg { width: 12px; height: 12px; display: block; }
10664
+ `;
10665
+
10666
+ // src/ui/components/number-input/view.ts
10667
+ var NumberInput = class {
10668
+ constructor(opts) {
10669
+ const doc = document;
10670
+ injectStyles(NUMBER_STYLE_ID, NUMBER_CSS, doc);
10671
+ this.ctrl = numberInputController(opts);
10672
+ this.placeholderMode = opts.placeholder !== void 0;
10673
+ this.emptyValue = opts.emptyValue ?? opts.value;
10674
+ this.last = String(opts.value);
10675
+ const wrap = doc.createElement("div");
10676
+ wrap.className = "vela-num";
10677
+ wrap.dataset.size = this.ctrl.size;
10678
+ if (this.ctrl.size !== "sm") wrap.dataset.fill = "";
10679
+ if (opts.compact) wrap.dataset.compact = "";
10680
+ if (this.ctrl.steppers) wrap.dataset.steppers = "";
10681
+ const ni = doc.createElement("input");
10682
+ ni.type = "number";
10683
+ if (opts.id) ni.id = opts.id;
10684
+ if (opts.title) ni.title = opts.title;
10685
+ if (opts.min !== void 0) ni.min = String(opts.min);
10686
+ if (opts.max !== void 0) ni.max = String(opts.max);
10687
+ ni.step = String(this.ctrl.step);
10688
+ if (this.ctrl.disabled) ni.disabled = true;
10689
+ if (this.placeholderMode) {
10690
+ ni.placeholder = opts.placeholder ?? "";
10691
+ ni.value = opts.value !== this.emptyValue ? String(opts.value) : "";
10692
+ } else {
10693
+ ni.value = String(opts.value);
10694
+ }
10695
+ const commitRaw = (raw) => {
10696
+ const n = this.ctrl.apply(raw);
10697
+ if (n === null) {
10698
+ ni.value = this.placeholderMode && this.ctrl.value === this.emptyValue ? "" : this.last;
10699
+ return;
10700
+ }
10701
+ this.last = String(n);
10702
+ if (this.placeholderMode && n === this.emptyValue) ni.value = "";
10703
+ else ni.value = String(n);
10704
+ };
10705
+ if (this.placeholderMode) {
10706
+ ni.addEventListener("change", () => {
10707
+ const raw = ni.value.trim() === "" ? this.emptyValue : Number(ni.value);
10708
+ commitRaw(Number.isFinite(raw) ? raw : this.emptyValue);
10709
+ });
10710
+ } else if (this.ctrl.commit === "live") {
10711
+ ni.addEventListener("input", () => {
10712
+ const v = Number(ni.value);
10713
+ if (Number.isFinite(v)) this.ctrl.apply(v);
10714
+ });
10715
+ } else {
10716
+ ni.addEventListener("blur", () => commitRaw(Number(ni.value)));
10717
+ ni.addEventListener("keydown", (e) => {
10718
+ if (e.key === "Enter") {
10719
+ e.preventDefault();
10720
+ ni.blur();
10721
+ }
10722
+ });
10723
+ }
10724
+ wrap.appendChild(ni);
10725
+ if (this.ctrl.steppers) {
10726
+ wrap.appendChild(this.buildSteppers(doc, (dir) => {
10727
+ const cur = Number(ni.value);
10728
+ const base = Number.isFinite(cur) ? cur : this.ctrl.value;
10729
+ commitRaw(snapToStep(base + dir * this.ctrl.step, base, this.ctrl.step));
10730
+ }));
10731
+ }
10732
+ this.el = wrap;
10733
+ this.input = ni;
10734
+ }
10735
+ get value() {
10736
+ return this.ctrl.value;
10737
+ }
10738
+ setValue(v) {
10739
+ const n = this.ctrl.sync(v);
10740
+ this.last = String(n);
10741
+ if (this.placeholderMode && n === this.emptyValue) this.input.value = "";
10742
+ else this.input.value = String(n);
10743
+ }
10744
+ buildSteppers(doc, applyDir) {
10745
+ const steps = doc.createElement("div");
10746
+ steps.className = "vela-num-step";
10747
+ const mk = (dir, icon2, label) => {
10748
+ const b = doc.createElement("button");
10749
+ b.type = "button";
10750
+ b.tabIndex = -1;
10751
+ b.setAttribute("aria-label", label);
10752
+ b.appendChild(iconEl(icon2, doc));
10753
+ let timer = 0;
10754
+ const apply = () => applyDir(dir);
10755
+ const stop = () => {
10756
+ if (timer) {
10757
+ window.clearTimeout(timer);
10758
+ timer = 0;
10759
+ }
10760
+ };
10761
+ b.addEventListener("pointerdown", (e) => {
10762
+ e.preventDefault();
10763
+ e.stopPropagation();
10764
+ apply();
10765
+ timer = window.setTimeout(function tick() {
10766
+ apply();
10767
+ timer = window.setTimeout(tick, 60);
10768
+ }, 400);
10769
+ });
10770
+ b.addEventListener("pointerup", stop);
10771
+ b.addEventListener("pointerleave", stop);
10772
+ b.addEventListener("pointercancel", stop);
10773
+ return b;
10774
+ };
10775
+ steps.append(mk(1, "chevron-up", "Increase"), mk(-1, "chevron-down", "Decrease"));
10776
+ return steps;
10777
+ }
10778
+ };
10779
+
10780
+ // src/ui/components/text-field/controller.ts
10781
+ function textFieldController(opts = {}) {
10782
+ let value = opts.value ?? "";
10783
+ const size = opts.size ?? "md";
10784
+ return {
10785
+ get value() {
10786
+ return value;
10787
+ },
10788
+ size,
10789
+ fill: opts.fill ?? size !== "sm",
10790
+ disabled: opts.disabled ?? false,
10791
+ commit(next) {
10792
+ if (next === value) return null;
10793
+ value = next;
10794
+ opts.onChange?.(next);
10795
+ return next;
10796
+ },
10797
+ sync(next) {
10798
+ value = next;
10799
+ }
10800
+ };
10801
+ }
10802
+
10803
+ // src/ui/components/text-field/styles.ts
10804
+ var TEXT_STYLE_ID = "vela-ui-text";
10805
+ var TEXT_CSS = `
10806
+ .vela-text {
10807
+ display: inline-block;
10808
+ min-width: 0;
10809
+ }
10810
+ .vela-text[data-fill] { width: 100%; }
10811
+ .vela-text-field {
10812
+ width: 100%;
10813
+ box-sizing: border-box;
10814
+ height: 34px;
10815
+ background: transparent;
10816
+ border: 1px solid var(--vela-border-strong);
10817
+ border-radius: 6px;
10818
+ color: var(--vela-fg-bright);
10819
+ padding: 0 8px;
10820
+ font-size: 14px;
10821
+ font-family: inherit;
10822
+ outline: none;
10823
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
10824
+ }
10825
+ .vela-text-field:hover { border-color: var(--vela-fg-muted); }
10826
+ .vela-text-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
10827
+ .vela-text[data-size='sm'] .vela-text-field {
10828
+ height: 28px;
10829
+ background: var(--vela-surface-elev);
10830
+ border-radius: var(--vela-radius-sm);
10831
+ color: var(--vela-fg);
10832
+ font-size: 13px;
10833
+ }
10834
+ `;
10835
+
10836
+ // src/ui/components/text-field/view.ts
10837
+ var TextField = class {
10838
+ constructor(opts = {}) {
10839
+ const doc = document;
10840
+ injectStyles(TEXT_STYLE_ID, TEXT_CSS, doc);
10841
+ this.ctrl = textFieldController(opts);
10842
+ const wrap = doc.createElement("div");
10843
+ wrap.className = "vela-text";
10844
+ wrap.dataset.size = this.ctrl.size;
10845
+ if (this.ctrl.fill) wrap.dataset.fill = "";
10846
+ const ti = doc.createElement("input");
10847
+ ti.type = "text";
10848
+ if (opts.id) ti.id = opts.id;
10849
+ ti.value = this.ctrl.value;
10850
+ ti.className = "vela-text-field";
10851
+ if (this.ctrl.disabled) ti.disabled = true;
10852
+ ti.addEventListener("blur", () => {
10853
+ this.ctrl.commit(ti.value);
10854
+ });
10855
+ ti.addEventListener("keydown", (e) => {
10856
+ if (e.key === "Enter") {
10857
+ e.preventDefault();
10858
+ ti.blur();
10859
+ }
10860
+ });
10861
+ wrap.appendChild(ti);
10862
+ this.el = wrap;
10863
+ this.input = ti;
10864
+ }
10865
+ get value() {
10866
+ return this.ctrl.value;
10867
+ }
10868
+ setValue(v) {
10869
+ this.ctrl.sync(v);
10870
+ this.input.value = v;
10871
+ }
10872
+ };
10873
+
10874
+ // src/ui/components/color-picker/controller.ts
10875
+ function splitColor(color) {
10876
+ const s = String(color ?? "").trim();
10877
+ let m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
10878
+ if (m) return { hex6: `#${m[1].toLowerCase()}`, alpha: m[2] ? parseInt(m[2], 16) / 255 : 1 };
10879
+ m = /^#([0-9a-f]{3})$/i.exec(s);
10880
+ if (m) {
10881
+ const h = m[1].toLowerCase();
10882
+ return { hex6: `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`, alpha: 1 };
10883
+ }
10884
+ const rgba = /^rgba?\(([^)]+)\)/i.exec(s);
10885
+ if (rgba) {
10886
+ const p = rgba[1].split(",").map((v) => v.trim());
10887
+ const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
10888
+ const a = p[3] != null ? Math.max(0, Math.min(1, parseFloat(p[3]))) : 1;
10889
+ return { hex6: `#${hx(parseInt(p[0] ?? "0", 10))}${hx(parseInt(p[1] ?? "0", 10))}${hx(parseInt(p[2] ?? "0", 10))}`, alpha: a };
10890
+ }
10891
+ return { hex6: ACCENT_BRIGHT, alpha: 1 };
10892
+ }
10893
+ function combineColor(hex6, alpha) {
10894
+ const a = Math.max(0, Math.min(1, alpha));
10895
+ if (a >= 0.999) return hex6;
10896
+ return `${hex6}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
10897
+ }
10898
+ function blendOver(fg, bg, alpha) {
10899
+ const rgb = (c) => {
10900
+ const n = parseInt(splitColor(c).hex6.slice(1), 16);
10901
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
10902
+ };
10903
+ const [fr, fgc, fb] = rgb(fg);
10904
+ const [br, bgc, bb] = rgb(bg);
10905
+ const a = Math.max(0, Math.min(1, alpha));
10906
+ const h = (n) => Math.round(n).toString(16).padStart(2, "0");
10907
+ return `#${h(fr * a + br * (1 - a))}${h(fgc * a + bgc * (1 - a))}${h(fb * a + bb * (1 - a))}`;
10908
+ }
10909
+ function hslHex(h, s, l) {
10910
+ const sn = s / 100;
10911
+ const ln = l / 100;
10912
+ const k = (n) => (n + h / 30) % 12;
10913
+ const a = sn * Math.min(ln, 1 - ln);
10914
+ const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
10915
+ const to = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
10916
+ return `#${to(f(0))}${to(f(8))}${to(f(4))}`;
10917
+ }
10918
+ function buildPalette() {
10919
+ const grays = [100, 90, 80, 68, 56, 45, 35, 25, 14, 0].map((l) => hslHex(0, 0, l));
10920
+ const hues = [4, 28, 50, 96, 140, 174, 200, 224, 270, 322];
10921
+ const levels = [
10922
+ [82, 56],
10923
+ [58, 84],
10924
+ [62, 74],
10925
+ [66, 62],
10926
+ [70, 50],
10927
+ [64, 39],
10928
+ [54, 29]
10929
+ ];
10930
+ return [grays, ...levels.map(([s, l]) => hues.map((h) => hslHex(h, s, l)))];
10931
+ }
10932
+ function transparencyChecker(size) {
10933
+ return `repeating-conic-gradient(#9aa0a6 0% 25%, #d3d6da 0% 50%) 0 0 / ${size}px ${size}px`;
10934
+ }
10935
+
10936
+ // src/ui/components/color-picker/styles.ts
10937
+ var COLOR_STYLE_ID = "vela-ui-color";
10938
+ var COLOR_CSS = `
10939
+ .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;}
10940
+ .vela-color-field:hover{border-color:var(--vela-fg-muted);}
10941
+ .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);}
10942
+ .vela-color-field-circle{width:26px;height:26px;padding:3px;border:1px solid var(--vela-border-strong);border-radius:4px;background:transparent;overflow:hidden;}
10943
+ .vela-color-field-circle .vela-color-field-swatch{border-radius:2px;box-shadow:none;}
10944
+ .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;}
10945
+ `;
10946
+
10947
+ // src/ui/components/color-picker/view.ts
10948
+ var PALETTE = buildPalette();
10949
+ var CHECKER = transparencyChecker(10);
10950
+ var FIELD_CHECKER = transparencyChecker(8);
10951
+ var recents = [ACCENT, BULLISH, BEARISH, WARNING, NEUTRAL];
10952
+ function addRecent(hex6) {
10953
+ const i = recents.findIndex((c) => c.toLowerCase() === hex6.toLowerCase());
10954
+ if (i >= 0) recents.splice(i, 1);
10955
+ recents.unshift(hex6);
10956
+ if (recents.length > 10) recents.length = 10;
10957
+ }
10958
+ function buildColorPicker(color, theme, onChange) {
10959
+ const parsed = splitColor(color);
10960
+ let curHex = parsed.hex6;
10961
+ let curAlpha = parsed.alpha;
10962
+ const root = document.createElement("div");
10963
+ root.style.cssText = "display:flex;flex-direction:column;gap:9px;width:236px;";
10964
+ const grid = document.createElement("div");
10965
+ grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
10966
+ const swatches = [];
10967
+ for (const row of PALETTE) {
10968
+ for (const c of row) {
10969
+ const sw = document.createElement("button");
10970
+ sw.type = "button";
10971
+ sw.dataset.c = c;
10972
+ sw.style.cssText = `width:100%;aspect-ratio:1;border-radius:4px;border:none;cursor:pointer;background:${c};padding:0;`;
10973
+ sw.addEventListener("click", (e) => {
10974
+ e.stopPropagation();
10975
+ pickHex(c);
10976
+ });
10977
+ grid.appendChild(sw);
10978
+ swatches.push(sw);
10979
+ }
10980
+ }
10981
+ const paintSelection = () => {
10982
+ for (const sw of swatches) {
10983
+ const on = (sw.dataset.c ?? "").toLowerCase() === curHex.toLowerCase();
10984
+ sw.style.outline = on ? `2px solid ${theme.textColor}` : "none";
10985
+ sw.style.outlineOffset = "2px";
10986
+ sw.style.zIndex = on ? "1" : "";
10987
+ sw.style.position = on ? "relative" : "";
10988
+ }
10989
+ };
10990
+ const recentRow = document.createElement("div");
10991
+ recentRow.style.cssText = "display:flex;align-items:center;gap:5px;flex-wrap:wrap;border-top:1px solid var(--vela-border);padding-top:9px;";
10992
+ const renderRecents = () => {
10993
+ recentRow.replaceChildren();
10994
+ for (const c of recents) {
10995
+ const sw = document.createElement("button");
10996
+ sw.type = "button";
10997
+ 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;`;
10998
+ sw.addEventListener("click", (e) => {
10999
+ e.stopPropagation();
11000
+ pickHex(c);
11001
+ });
11002
+ recentRow.appendChild(sw);
11003
+ }
11004
+ const add = document.createElement("label");
11005
+ 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;`;
11006
+ add.textContent = "+";
11007
+ const input = document.createElement("input");
11008
+ input.type = "color";
11009
+ input.value = curHex;
11010
+ input.style.cssText = "position:absolute;inset:0;opacity:0;cursor:pointer;";
11011
+ input.addEventListener("input", () => pickHex(input.value, true));
11012
+ add.appendChild(input);
11013
+ recentRow.appendChild(add);
11014
+ };
11015
+ const opLabel = document.createElement("div");
11016
+ opLabel.textContent = "Opacity";
11017
+ opLabel.style.cssText = `font:11px ${theme.fontFamily};color:var(--vela-fg-muted);`;
11018
+ const opRow = document.createElement("div");
11019
+ opRow.style.cssText = "display:flex;align-items:center;gap:10px;";
11020
+ const track = document.createElement("div");
11021
+ track.style.cssText = "flex:1;position:relative;height:13px;border-radius:7px;cursor:pointer;";
11022
+ const knob = document.createElement("div");
11023
+ 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;";
11024
+ track.appendChild(knob);
11025
+ const pctBox = document.createElement("div");
11026
+ 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;`;
11027
+ opRow.append(track, pctBox);
11028
+ const paintOpacity = () => {
11029
+ track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
11030
+ knob.style.left = `${curAlpha * 100}%`;
11031
+ pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
11032
+ };
11033
+ let dragging = false;
11034
+ const onDrag = (clientX) => {
11035
+ const r = track.getBoundingClientRect();
11036
+ curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
11037
+ paintOpacity();
11038
+ emit();
11039
+ };
11040
+ track.addEventListener("pointerdown", (e) => {
11041
+ e.stopPropagation();
11042
+ dragging = true;
11043
+ track.setPointerCapture(e.pointerId);
11044
+ onDrag(e.clientX);
11045
+ });
11046
+ track.addEventListener("pointermove", (e) => {
11047
+ if (dragging) onDrag(e.clientX);
11048
+ });
11049
+ track.addEventListener("pointerup", () => dragging = false);
11050
+ function emit() {
11051
+ onChange(combineColor(curHex, curAlpha));
11052
+ }
11053
+ function pickHex(hex, custom = false) {
11054
+ curHex = splitColor(hex).hex6;
11055
+ addRecent(curHex);
11056
+ paintSelection();
11057
+ paintOpacity();
11058
+ if (custom) renderRecents();
11059
+ emit();
11060
+ }
11061
+ renderRecents();
11062
+ paintSelection();
11063
+ paintOpacity();
11064
+ root.append(grid, recentRow, opLabel, opRow);
11065
+ return root;
11066
+ }
11067
+ function colorField(theme, getVal, onVal, opts) {
11068
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
11069
+ }
11070
+ var ColorField = class {
11071
+ constructor(opts) {
11072
+ injectStyles(COLOR_STYLE_ID, COLOR_CSS, document);
11073
+ this.theme = opts.theme;
11074
+ this.getVal = opts.getVal;
11075
+ this.onVal = opts.onVal;
11076
+ this.popoverOpts = opts.popover;
11077
+ const trigger = document.createElement("button");
11078
+ trigger.type = "button";
11079
+ if (opts.id) trigger.id = opts.id;
11080
+ trigger.className = opts.shape === "circle" ? "vela-color-field vela-color-field-circle" : "vela-color-field";
11081
+ const swatch = document.createElement("span");
11082
+ swatch.className = "vela-color-field-swatch";
11083
+ trigger.appendChild(swatch);
11084
+ this.el = trigger;
11085
+ this.swatch = swatch;
11086
+ this.paint();
11087
+ trigger.addEventListener("vela-sync", () => this.paint());
11088
+ trigger.addEventListener("pointerdown", (e) => {
11089
+ e.preventDefault();
11090
+ trigger.focus({ preventScroll: true });
11091
+ });
11092
+ trigger.addEventListener("click", (e) => {
11093
+ e.stopPropagation();
11094
+ this.toggle();
11095
+ });
11096
+ }
11097
+ paint() {
11098
+ const v = this.getVal();
11099
+ this.swatch.style.background = `linear-gradient(${v}, ${v}), ${FIELD_CHECKER}`;
11100
+ }
11101
+ toggle() {
11102
+ if (openPopoverTrigger() === this.el) {
11103
+ closeOpenPopovers();
11104
+ return;
11105
+ }
11106
+ const pop = new Popover({
11107
+ trigger: this.el,
11108
+ theme: this.theme,
11109
+ align: this.popoverOpts?.align ?? "end",
11110
+ gap: this.popoverOpts?.gap ?? 6,
11111
+ host: this.popoverOpts?.host,
11112
+ position: this.popoverOpts?.position,
11113
+ boundary: this.popoverOpts?.boundary,
11114
+ zIndex: this.popoverOpts?.zIndex,
11115
+ className: "vela-color-field-pop",
11116
+ content: buildColorPicker(this.getVal(), this.theme, (val) => {
11117
+ this.onVal(val);
11118
+ this.paint();
11119
+ })
11120
+ });
11121
+ pop.show();
11122
+ }
11123
+ };
11124
+
9884
11125
  // src/renderers/shared/InputsUI.ts
9885
11126
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
9886
11127
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
@@ -9889,6 +11130,7 @@ var LEGEND_CTL_PX = 18;
9889
11130
  var LEGEND_ROW_PAD_Y = 2;
9890
11131
  var LEGEND_ROW_PAD_X = 6;
9891
11132
  var LEGEND_TITLE_VALUES_GAP_PX = 6;
11133
+ var LEGEND_TITLE_STATUS_GAP_PX = 8;
9892
11134
  var LEGEND_ACTION_GAP_PX = 6;
9893
11135
  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
11136
  var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
@@ -9933,6 +11175,11 @@ var InputsUI = class {
9933
11175
  this.dialogTips = [];
9934
11176
  /** Open "Move to" menu (kept so it can be torn down). */
9935
11177
  this.moveMenu = null;
11178
+ /** Open settings-dialog choice menu (custom dropdown — torn down with the dialog). */
11179
+ this.choicePop = null;
11180
+ /** Open settings-dialog calendar popover (torn down with the dialog). */
11181
+ this.calendarPop = null;
11182
+ this.calendarAnchor = null;
9936
11183
  /** Collapsed panes → the master indicator id to keep visible (others hidden in the strip). */
9937
11184
  this.paneCollapse = /* @__PURE__ */ new Map();
9938
11185
  /** The user folded the indicator legend away behind the chevron — CHART-WIDE: every
@@ -9958,6 +11205,10 @@ var InputsUI = class {
9958
11205
  this.onDialogKey = (e) => {
9959
11206
  if (e.key === "Escape") {
9960
11207
  e.preventDefault();
11208
+ if (isPopoverOpen()) {
11209
+ closeOpenPopovers();
11210
+ return;
11211
+ }
9961
11212
  this.closeDialog();
9962
11213
  }
9963
11214
  };
@@ -10403,6 +11654,7 @@ var InputsUI = class {
10403
11654
  titleWrap.appendChild(beta);
10404
11655
  }
10405
11656
  el.appendChild(titleWrap);
11657
+ el.appendChild(statusEl);
10406
11658
  const valuesEl = document.createElement("span");
10407
11659
  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
11660
  el.appendChild(valuesEl);
@@ -10463,7 +11715,6 @@ var InputsUI = class {
10463
11715
  close.addEventListener("click", () => this.onRemove?.(id));
10464
11716
  controlsEl.appendChild(close);
10465
11717
  el.appendChild(controlsEl);
10466
- el.appendChild(statusEl);
10467
11718
  this.attach(this.legendFor(paneId), el, !!opts.native);
10468
11719
  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
11720
  this.syncFoldToggle();
@@ -10481,7 +11732,7 @@ var InputsUI = class {
10481
11732
  /**
10482
11733
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
10483
11734
  * 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.
11735
+ * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered just right of the title.
10485
11736
  */
10486
11737
  setStatus(id, status) {
10487
11738
  const row = this.rows.get(id);
@@ -10493,7 +11744,7 @@ var InputsUI = class {
10493
11744
  return;
10494
11745
  }
10495
11746
  if (status === "loading") {
10496
- el.style.cssText = "display:inline-flex;align-items:center;gap:3px;box-sizing:border-box;flex:none;";
11747
+ 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
11748
  for (let i = 0; i < 3; i += 1) {
10498
11749
  const dot = document.createElement("span");
10499
11750
  dot.style.cssText = "width:4px;height:4px;border-radius:50%;background:currentColor;opacity:0.15;flex:none;";
@@ -10503,7 +11754,7 @@ var InputsUI = class {
10503
11754
  return;
10504
11755
  }
10505
11756
  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;`;
11757
+ 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
11758
  }
10508
11759
  /** Inject the status keyframes once (idempotent). */
10509
11760
  ensureStatusKeyframes() {
@@ -10556,15 +11807,15 @@ var InputsUI = class {
10556
11807
  * construction and is shown/hidden with the cluster container.
10557
11808
  */
10558
11809
  syncRowActions(row) {
10559
- const open = row.highlighted;
10560
- row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
11810
+ const open2 = row.highlighted;
11811
+ row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
10561
11812
  for (const child of Array.from(row.controlsEl.children)) {
10562
11813
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
10563
11814
  if (child === row.extrasEl) {
10564
- child.style.display = open ? "contents" : "none";
11815
+ child.style.display = open2 ? "contents" : "none";
10565
11816
  continue;
10566
11817
  }
10567
- child.style.display = open ? "inline-flex" : "none";
11818
+ child.style.display = open2 ? "inline-flex" : "none";
10568
11819
  }
10569
11820
  }
10570
11821
  /** Drop the current selection outline, if any. */
@@ -10618,7 +11869,8 @@ var InputsUI = class {
10618
11869
  /**
10619
11870
  * Open the indicator's settings as a centered modal over the chart — a themed,
10620
11871
  * 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=`
11872
+ * indicator title, a tab strip (the default "Inputs" tab plus any tab the inputs
11873
+ * declare via `tab=`), a scrollable body per tab that groups inputs by `group=`
10622
11874
  * (section headers) and packs `inline=` inputs onto one row, and a sticky footer.
10623
11875
  *
10624
11876
  * Edits commit LIVE (each change re-runs the indicator), matching the current
@@ -10648,55 +11900,57 @@ var InputsUI = class {
10648
11900
  });
10649
11901
  const card = document.createElement("div");
10650
11902
  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;`;
11903
+ 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
11904
  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
11905
  applyChromeTokens(card, t);
10654
11906
  card.addEventListener("keydown", (e) => {
10655
11907
  if (e.key !== "Escape") e.stopPropagation();
10656
11908
  });
10657
11909
  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;";
11910
+ header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 20px;flex:0 0 auto;";
10659
11911
  const hTitle = document.createElement("span");
10660
11912
  hTitle.textContent = row.settingsTitle;
10661
- hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
11913
+ hTitle.style.cssText = "font-weight:600;font-size:20px;line-height:28px;";
10662
11914
  const closeBtn = document.createElement("button");
10663
11915
  closeBtn.type = "button";
10664
11916
  closeBtn.setAttribute("aria-label", "Close");
10665
11917
  this.dialogTips.push(attachChromeTooltip(closeBtn, { host: this.container, theme: () => this.theme, text: () => "Close" }));
10666
11918
  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;";
11919
+ closeBtn.className = "vela-ind-dlg-close";
10669
11920
  closeBtn.addEventListener("click", () => this.closeDialog());
10670
11921
  header.append(hTitle, closeBtn);
10671
11922
  card.appendChild(header);
10672
11923
  if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
11924
+ const tabDefs = tabInputs(row.inputs);
10673
11925
  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);
11926
+ tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
11927
+ const tabEls = [];
11928
+ const bodies = [];
11929
+ const activate = (idx) => {
11930
+ for (let i = 0; i < tabEls.length; i += 1) {
11931
+ const active = i === idx;
11932
+ const el = tabEls[i];
11933
+ el.style.borderBottomColor = active ? fg : "transparent";
11934
+ el.style.color = active ? "inherit" : "var(--vela-fg-muted)";
11935
+ el.classList.toggle("vela-ind-tab-active", active);
11936
+ bodies[i].style.display = active ? "grid" : "none";
10690
11937
  }
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);
11938
+ };
11939
+ for (const [idx, def] of tabDefs.entries()) {
11940
+ const tab = document.createElement("div");
11941
+ tab.textContent = def.name;
11942
+ tab.className = "vela-ind-tab";
11943
+ 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;" : ""}`;
11944
+ tab.addEventListener("click", () => activate(idx));
11945
+ tabs.appendChild(tab);
11946
+ tabEls.push(tab);
11947
+ bodies.push(this.buildTabBody(def.inputs, row));
10696
11948
  }
10697
- card.appendChild(body);
11949
+ card.appendChild(tabs);
11950
+ for (const body of bodies) card.appendChild(body);
11951
+ activate(0);
10698
11952
  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;`;
11953
+ footer.style.cssText = `display:flex;justify-content:flex-end;gap:12px;padding:16px 20px;border-top:1px solid ${border};flex:0 0 auto;`;
10700
11954
  footer.append(
10701
11955
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
10702
11956
  this.dialogButton("Ok", true, () => this.closeDialog())
@@ -10708,8 +11962,32 @@ var InputsUI = class {
10708
11962
  this.backdrop = backdrop;
10709
11963
  document.addEventListener("keydown", this.onDialogKey);
10710
11964
  }
11965
+ /** One tab's scrollable body — one shared grid so every group's controls share a
11966
+ * left edge (a 100px number field lines up with a wider session/time pair). */
11967
+ buildTabBody(inputs, row) {
11968
+ const body = document.createElement("div");
11969
+ 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;";
11970
+ for (const group of groupInputs(inputs)) {
11971
+ const section = document.createElement("div");
11972
+ section.style.cssText = "display:contents;";
11973
+ if (group.name) {
11974
+ const gh = document.createElement("div");
11975
+ gh.textContent = group.name;
11976
+ const first = body.childElementCount === 0;
11977
+ 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;`;
11978
+ section.appendChild(gh);
11979
+ }
11980
+ for (const inputRow of group.rows) this.buildInputRow(section, inputRow, row);
11981
+ body.appendChild(section);
11982
+ }
11983
+ return body;
11984
+ }
10711
11985
  closeDialog() {
10712
11986
  document.removeEventListener("keydown", this.onDialogKey);
11987
+ closeOpenPopovers();
11988
+ this.choicePop = null;
11989
+ this.calendarPop = null;
11990
+ this.calendarAnchor = null;
10713
11991
  for (const dispose of this.dialogTips.splice(0)) dispose();
10714
11992
  this.backdrop?.remove();
10715
11993
  this.backdrop = null;
@@ -10737,79 +12015,77 @@ var InputsUI = class {
10737
12015
  buildInputRow(grid, decls, row) {
10738
12016
  const idOf = (inp) => `vela-inp-${row.id}-${inp.key}`;
10739
12017
  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;";
12018
+ const cell2 = document.createElement("div");
12019
+ cell2.style.cssText = "grid-column:1 / -1;display:flex;flex-wrap:wrap;align-items:center;gap:12px;";
10742
12020
  for (const d of decls) {
10743
12021
  const fit2 = d.type === "color" || d.type === "bool";
10744
12022
  const toggleFirst = d.type === "bool";
10745
12023
  const id2 = idOf(d);
10746
12024
  const item = document.createElement("div");
10747
- item.style.cssText = `display:flex;align-items:center;gap:6px;${toggleFirst ? "flex-direction:row-reverse;" : ""}`;
12025
+ item.style.cssText = `display:flex;align-items:center;gap:8px;${toggleFirst ? "flex-direction:row-reverse;" : ""}`;
10748
12026
  const name = nameOf(d);
10749
12027
  if (name) {
10750
12028
  const lbl = document.createElement("label");
10751
12029
  lbl.htmlFor = id2;
10752
12030
  lbl.textContent = name;
10753
- lbl.style.cssText = `font-size:13px;opacity:0.9;${toggleFirst ? "cursor:pointer;" : ""}`;
12031
+ lbl.style.cssText = `font-size:14px;opacity:0.9;${toggleFirst ? "cursor:pointer;" : ""}`;
10754
12032
  item.appendChild(lbl);
10755
12033
  }
10756
12034
  const cw2 = document.createElement("div");
10757
- cw2.style.cssText = fit2 ? "" : "width:110px;";
12035
+ cw2.style.cssText = fit2 ? "" : "width:100px;";
10758
12036
  cw2.appendChild(this.buildControl(row, d, id2));
10759
12037
  item.appendChild(cw2);
10760
- cell.appendChild(item);
12038
+ cell2.appendChild(item);
10761
12039
  }
10762
12040
  const lastTip = [...decls].reverse().find((d) => d.tooltip)?.tooltip;
10763
- if (lastTip) cell.appendChild(this.infoButton(lastTip));
10764
- grid.appendChild(cell);
12041
+ if (lastTip) cell2.appendChild(this.infoButton(lastTip));
12042
+ grid.appendChild(cell2);
10765
12043
  return;
10766
12044
  }
10767
12045
  const lead = decls[0];
10768
12046
  const id = idOf(lead);
10769
12047
  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;";
12048
+ const cell2 = document.createElement("div");
12049
+ cell2.style.cssText = "grid-column:1 / -1;display:flex;align-items:center;gap:8px;";
10772
12050
  const lbl = document.createElement("label");
10773
- lbl.style.cssText = "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;";
12051
+ lbl.style.cssText = "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;";
10774
12052
  lbl.appendChild(this.buildControl(row, lead, id));
10775
12053
  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"));
12054
+ cell2.appendChild(lbl);
12055
+ if (lead.tooltip) cell2.appendChild(this.infoButton(lead.tooltip));
12056
+ grid.appendChild(cell2);
10780
12057
  return;
10781
12058
  }
10782
12059
  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;";
12060
+ const cell2 = document.createElement("div");
12061
+ cell2.style.cssText = "grid-column:1 / -1;display:flex;flex-direction:column;gap:8px;";
10785
12062
  const head = document.createElement("div");
10786
12063
  head.style.cssText = "display:flex;align-items:center;justify-content:center;gap:8px;";
10787
12064
  const lbl = document.createElement("label");
10788
12065
  lbl.htmlFor = id;
10789
12066
  lbl.textContent = nameOf(lead);
10790
- lbl.style.cssText = "font-size:13px;";
12067
+ lbl.style.cssText = "font-size:14px;";
10791
12068
  head.appendChild(lbl);
10792
12069
  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);
12070
+ cell2.appendChild(head);
12071
+ cell2.appendChild(this.buildControl(row, lead, id));
12072
+ grid.appendChild(cell2);
10796
12073
  return;
10797
12074
  }
10798
12075
  const label = document.createElement("label");
10799
12076
  label.htmlFor = id;
10800
12077
  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;";
12078
+ label.style.cssText = "font-size:14px;opacity:0.9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
10804
12079
  const fit = lead.type === "color" || lead.type === "session" || lead.type === "time";
12080
+ const cell = document.createElement("div");
12081
+ cell.style.cssText = "display:flex;align-items:center;gap:8px;justify-self:start;";
10805
12082
  const cw = document.createElement("div");
10806
- cw.style.cssText = fit ? "" : "width:140px;";
12083
+ cw.style.cssText = fit ? "" : "width:100px;";
10807
12084
  cw.appendChild(this.buildControl(row, lead, id));
10808
- controlCell.appendChild(cw);
10809
- if (lead.tooltip) controlCell.appendChild(this.infoButton(lead.tooltip));
12085
+ cell.appendChild(cw);
12086
+ if (lead.tooltip) cell.appendChild(this.infoButton(lead.tooltip));
10810
12087
  grid.appendChild(label);
10811
- grid.appendChild(controlCell);
10812
- grid.appendChild(document.createElement("div"));
12088
+ grid.appendChild(cell);
10813
12089
  }
10814
12090
  /** Build the typed control for one input, committing edits live via `onChange`. */
10815
12091
  buildControl(row, inp, id) {
@@ -10818,107 +12094,30 @@ var InputsUI = class {
10818
12094
  row.values[inp.key] = value;
10819
12095
  this.onChange?.({ indicatorId: row.id, key: inp.key, value });
10820
12096
  };
10821
- const ctrl = this.ctrlStyle();
10822
12097
  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);
12098
+ if (inp.options && inp.options.length > 0) return this.select(id, inp.options.map(String), String(current), emit);
12099
+ if (inp.type === "source") return this.select(id, SOURCES, String(current), emit);
10825
12100
  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;
12101
+ const field = colorField(this.theme, () => String(row.values[inp.key] ?? inp.defval), (v) => emit(v), { shape: "circle", id });
12102
+ return field;
10833
12103
  }
10834
12104
  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);
12105
+ if (inp.type === "timeframe") return this.selectPairs(id, TIMEFRAME_OPTIONS, String(current), emit);
10836
12106
  if (inp.type === "session") return this.buildSession(id, String(current), emit);
10837
12107
  if (inp.type === "time") return this.buildTime(id, Number(current) || 0, emit);
10838
12108
  if (inp.type === "text_area") return this.buildTextArea(id, String(current), emit);
10839
12109
  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;
12110
+ return this.buildNumber(id, Number(current), inp, emit);
10872
12111
  }
10873
12112
  return this.buildTextField(id, String(current), emit);
10874
12113
  }
10875
12114
  /** A plain text field that commits on blur / Enter (shared by string inputs and the pickerless symbol field). */
10876
12115
  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;
12116
+ return new TextField({ id, value: current, size: "md", onChange: emit }).el;
10896
12117
  }
10897
12118
  /** A square check-toggle (filled + check when on) — replaces the raw checkbox. */
10898
12119
  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;
12120
+ return new Switch({ id, checked, size: "md", tone: "bright", onChange }).el;
10922
12121
  }
10923
12122
  /** A hoverable ⓘ affordance carrying an input's `tooltip` (themed chrome tip; wraps). */
10924
12123
  infoButton(tooltip2) {
@@ -10930,45 +12129,22 @@ var InputsUI = class {
10930
12129
  return b;
10931
12130
  }
10932
12131
  /** 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);
12132
+ select(id, options, current, emit) {
12133
+ return this.selectPairs(id, options.map((o) => ({ value: o, label: o })), current, emit);
10935
12134
  }
10936
12135
  /** 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;
12136
+ selectPairs(id, pairs, current, onChange) {
12137
+ return new Select({
12138
+ id,
12139
+ options: pairs,
12140
+ value: current,
12141
+ size: "md",
12142
+ theme: this.theme,
12143
+ list: this.listPopover(),
12144
+ onChange
12145
+ }).el;
10970
12146
  }
10971
- /** `input.session` → two HH:MM dropdowns committing a `HHMM-HHMM` session string. */
12147
+ /** `input.session` → two typeable HH:MM comboboxes committing a `HHMM-HHMM` session string. */
10972
12148
  buildSession(id, current, emit) {
10973
12149
  const [start, end] = sessionToTimes(current);
10974
12150
  const wrap = document.createElement("div");
@@ -10976,15 +12152,16 @@ var InputsUI = class {
10976
12152
  let from = start;
10977
12153
  let to = end;
10978
12154
  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);
12155
+ wrap.append(
12156
+ this.timeCombobox(id, from, 86, (v) => {
12157
+ from = v;
12158
+ commit();
12159
+ }),
12160
+ this.timeCombobox(`${id}-end`, to, 86, (v) => {
12161
+ to = v;
12162
+ commit();
12163
+ })
12164
+ );
10988
12165
  return wrap;
10989
12166
  }
10990
12167
  /** `input.time` → a date picker + an HH:MM dropdown, committing an epoch-ms timestamp. */
@@ -10999,20 +12176,15 @@ var InputsUI = class {
10999
12176
  const ms = Date.parse(`${date}T${time}:00`);
11000
12177
  if (Number.isFinite(ms)) emit(ms);
11001
12178
  };
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;
12179
+ const dateField = this.dateField(id, date, (v) => {
12180
+ date = v;
11009
12181
  commit();
11010
12182
  });
11011
- const timeSel = this.timeSelect(`${id}-time`, time, 86, (v) => {
12183
+ const timeSel = this.timeCombobox(`${id}-time`, time, 86, (v) => {
11012
12184
  time = v;
11013
12185
  commit();
11014
12186
  });
11015
- wrap.append(dateInput, timeSel);
12187
+ wrap.append(dateField, timeSel);
11016
12188
  return wrap;
11017
12189
  }
11018
12190
  /** `input.text_area` → a multi-line textarea, committed on blur. */
@@ -11031,9 +12203,261 @@ var InputsUI = class {
11031
12203
  });
11032
12204
  return ta;
11033
12205
  }
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);
12206
+ /** A fixed-width HH:MM combobox (typeable, plus a 30-minute-step dropdown) used by session + time. */
12207
+ timeCombobox(id, current, width, onChange) {
12208
+ const wrap = document.createElement("div");
12209
+ wrap.className = "vela-ind-combo";
12210
+ wrap.style.cssText = `position:relative;width:${width}px;`;
12211
+ const input = document.createElement("input");
12212
+ input.type = "text";
12213
+ input.id = id;
12214
+ input.value = current;
12215
+ input.autocomplete = "off";
12216
+ input.spellcheck = false;
12217
+ input.style.cssText = `${this.ctrlStyle()}padding-right:26px;`;
12218
+ const chevron = document.createElement("button");
12219
+ chevron.type = "button";
12220
+ chevron.tabIndex = -1;
12221
+ chevron.className = "vela-ind-combo-chevron";
12222
+ chevron.setAttribute("aria-label", "Open time list");
12223
+ chevron.innerHTML = CLOCK_SVG;
12224
+ let value = current;
12225
+ const commitTyped = () => {
12226
+ const next = normalizeTimeInput(input.value);
12227
+ if (!next) {
12228
+ input.value = value;
12229
+ return;
12230
+ }
12231
+ input.value = next;
12232
+ if (next !== value) {
12233
+ value = next;
12234
+ onChange(next);
12235
+ }
12236
+ };
12237
+ input.addEventListener("blur", () => {
12238
+ if (isPopoverOpen()) return;
12239
+ commitTyped();
12240
+ });
12241
+ const pick = (v) => {
12242
+ value = v;
12243
+ input.value = v;
12244
+ onChange(v);
12245
+ };
12246
+ const openList = () => {
12247
+ this.choicePop = toggleSelectList(wrap, TIME_OPTIONS, value, pick, {
12248
+ ...this.listPopover(),
12249
+ onClose: () => {
12250
+ this.choicePop = null;
12251
+ }
12252
+ });
12253
+ };
12254
+ input.addEventListener("keydown", (e) => {
12255
+ if (e.key === "Enter") {
12256
+ e.preventDefault();
12257
+ commitTyped();
12258
+ input.blur();
12259
+ }
12260
+ if (e.key === "ArrowDown") {
12261
+ e.preventDefault();
12262
+ openList();
12263
+ }
12264
+ });
12265
+ const open2 = (e) => {
12266
+ e.preventDefault();
12267
+ e.stopPropagation();
12268
+ openList();
12269
+ };
12270
+ input.addEventListener("click", open2);
12271
+ chevron.addEventListener("mousedown", (e) => e.preventDefault());
12272
+ chevron.addEventListener("click", open2);
12273
+ wrap.append(input, chevron);
12274
+ return wrap;
12275
+ }
12276
+ /** A typeable YYYY-MM-DD field that also opens a themed month calendar. */
12277
+ dateField(id, current, onChange) {
12278
+ const wrap = document.createElement("div");
12279
+ wrap.className = "vela-ind-combo";
12280
+ wrap.style.cssText = "position:relative;width:128px;";
12281
+ const input = document.createElement("input");
12282
+ input.type = "text";
12283
+ input.id = id;
12284
+ input.value = current;
12285
+ input.autocomplete = "off";
12286
+ input.spellcheck = false;
12287
+ input.style.cssText = `${this.ctrlStyle()}padding-right:26px;`;
12288
+ const chevron = document.createElement("button");
12289
+ chevron.type = "button";
12290
+ chevron.tabIndex = -1;
12291
+ chevron.className = "vela-ind-combo-chevron";
12292
+ chevron.setAttribute("aria-label", "Open calendar");
12293
+ chevron.innerHTML = CALENDAR_SVG;
12294
+ let value = current;
12295
+ const commitTyped = () => {
12296
+ const next = normalizeDateInput(input.value);
12297
+ if (!next) {
12298
+ input.value = value;
12299
+ return;
12300
+ }
12301
+ input.value = next;
12302
+ if (next !== value) {
12303
+ value = next;
12304
+ onChange(next);
12305
+ }
12306
+ };
12307
+ input.addEventListener("blur", () => {
12308
+ if (isPopoverOpen()) return;
12309
+ commitTyped();
12310
+ });
12311
+ const pick = (v) => {
12312
+ value = v;
12313
+ input.value = v;
12314
+ onChange(v);
12315
+ };
12316
+ input.addEventListener("keydown", (e) => {
12317
+ if (e.key === "Enter") {
12318
+ e.preventDefault();
12319
+ commitTyped();
12320
+ input.blur();
12321
+ }
12322
+ if (e.key === "ArrowDown") {
12323
+ e.preventDefault();
12324
+ this.openCalendar(wrap, value, pick);
12325
+ }
12326
+ });
12327
+ const open2 = (e) => {
12328
+ e.preventDefault();
12329
+ e.stopPropagation();
12330
+ if (openPopoverTrigger() === wrap) {
12331
+ closeOpenPopovers();
12332
+ return;
12333
+ }
12334
+ this.openCalendar(wrap, value, pick);
12335
+ };
12336
+ input.addEventListener("click", open2);
12337
+ chevron.addEventListener("mousedown", (e) => e.preventDefault());
12338
+ chevron.addEventListener("click", open2);
12339
+ wrap.append(input, chevron);
12340
+ return wrap;
12341
+ }
12342
+ /** Numeric field with hover steppers on the right (no fill until a triangle is hovered). */
12343
+ buildNumber(id, current, inp, emit) {
12344
+ return new NumberInput({
12345
+ id,
12346
+ value: current,
12347
+ min: inp.min,
12348
+ max: inp.max,
12349
+ step: inp.step ?? (inp.type === "int" ? 1 : 0.1),
12350
+ integer: inp.type === "int",
12351
+ size: "md",
12352
+ commit: "blur",
12353
+ onChange: emit
12354
+ }).el;
12355
+ }
12356
+ /** Placement shared by the indicator dialog's dropdowns and the time combobox. */
12357
+ listPopover() {
12358
+ return {
12359
+ theme: this.theme,
12360
+ matchWidth: true,
12361
+ boundary: this.dialog ?? "viewport",
12362
+ boundaryInset: 8,
12363
+ gap: 4
12364
+ };
12365
+ }
12366
+ /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
12367
+ openCalendar(anchor, current, onPick) {
12368
+ const parsed = parseIsoDate(current);
12369
+ let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
12370
+ let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
12371
+ const selected = parsed ? isoDate(parsed) : current;
12372
+ const pop = new Popover({
12373
+ trigger: anchor,
12374
+ theme: this.theme,
12375
+ className: "vela-ind-cal",
12376
+ boundary: this.dialog ?? "viewport",
12377
+ boundaryInset: 8,
12378
+ gap: 4,
12379
+ onClose: () => {
12380
+ this.calendarPop = null;
12381
+ this.calendarAnchor = null;
12382
+ },
12383
+ content: (el) => {
12384
+ const title = document.createElement("div");
12385
+ title.className = "vela-ind-cal-title";
12386
+ const prev2 = document.createElement("button");
12387
+ prev2.type = "button";
12388
+ prev2.className = "vela-ind-cal-nav";
12389
+ prev2.setAttribute("aria-label", "Previous month");
12390
+ prev2.innerHTML = iconAt("chevron-left", 14);
12391
+ const next = document.createElement("button");
12392
+ next.type = "button";
12393
+ next.className = "vela-ind-cal-nav";
12394
+ next.setAttribute("aria-label", "Next month");
12395
+ next.innerHTML = iconAt("chevron-right", 14);
12396
+ const head = document.createElement("div");
12397
+ head.className = "vela-ind-cal-head";
12398
+ head.append(prev2, title, next);
12399
+ const week = document.createElement("div");
12400
+ week.className = "vela-ind-cal-week";
12401
+ for (const d of WEEKDAY_LABELS) {
12402
+ const cell = document.createElement("span");
12403
+ cell.textContent = d;
12404
+ week.appendChild(cell);
12405
+ }
12406
+ const grid = document.createElement("div");
12407
+ grid.className = "vela-ind-cal-grid";
12408
+ const paint = () => {
12409
+ title.textContent = `${MONTH_LABELS[month]} ${year}`;
12410
+ grid.replaceChildren();
12411
+ const first = new Date(year, month, 1);
12412
+ const startPad = first.getDay();
12413
+ const days = new Date(year, month + 1, 0).getDate();
12414
+ const today = isoDate(/* @__PURE__ */ new Date());
12415
+ for (let i = 0; i < startPad; i++) {
12416
+ const blank = document.createElement("span");
12417
+ blank.className = "vela-ind-cal-blank";
12418
+ grid.appendChild(blank);
12419
+ }
12420
+ for (let day = 1; day <= days; day++) {
12421
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
12422
+ const b = document.createElement("button");
12423
+ b.type = "button";
12424
+ b.className = "vela-ind-cal-day";
12425
+ b.textContent = String(day);
12426
+ if (iso === selected) b.dataset.checked = "1";
12427
+ if (iso === today) b.dataset.today = "1";
12428
+ b.addEventListener("click", (e) => {
12429
+ e.stopPropagation();
12430
+ pop.hide();
12431
+ onPick(iso);
12432
+ });
12433
+ grid.appendChild(b);
12434
+ }
12435
+ };
12436
+ prev2.addEventListener("click", (e) => {
12437
+ e.stopPropagation();
12438
+ month -= 1;
12439
+ if (month < 0) {
12440
+ month = 11;
12441
+ year -= 1;
12442
+ }
12443
+ paint();
12444
+ });
12445
+ next.addEventListener("click", (e) => {
12446
+ e.stopPropagation();
12447
+ month += 1;
12448
+ if (month > 11) {
12449
+ month = 0;
12450
+ year += 1;
12451
+ }
12452
+ paint();
12453
+ });
12454
+ paint();
12455
+ el.append(head, week, grid);
12456
+ }
12457
+ });
12458
+ this.calendarPop = pop;
12459
+ this.calendarAnchor = anchor;
12460
+ pop.show();
11037
12461
  }
11038
12462
  /**
11039
12463
  * `input.symbol` → a field that opens the host's ticker-selection UI when a picker is wired
@@ -11061,7 +12485,7 @@ var InputsUI = class {
11061
12485
  }
11062
12486
  /** Shared field chrome for text / number / select controls (fills its wrapper's width). */
11063
12487
  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;`;
12488
+ 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
12489
  }
11066
12490
  /** Whether the active theme is dark (drives the dialog's `color-scheme`). */
11067
12491
  isDarkTheme() {
@@ -11080,21 +12504,6 @@ var InputsUI = class {
11080
12504
  strongText() {
11081
12505
  return "var(--vela-fg-bright)";
11082
12506
  }
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
12507
  dialogButton(label, primary, onClick) {
11099
12508
  const b = document.createElement("button");
11100
12509
  b.type = "button";
@@ -11108,6 +12517,21 @@ function nameOf(inp) {
11108
12517
  const t = inp.title;
11109
12518
  return t.length > 0 ? t.charAt(0).toUpperCase() + t.slice(1) : "";
11110
12519
  }
12520
+ var DEFAULT_INPUT_TAB = "Inputs";
12521
+ function tabInputs(inputs) {
12522
+ const byTab = /* @__PURE__ */ new Map();
12523
+ for (const inp of inputs) {
12524
+ const name = inp.tab && inp.tab.length > 0 ? inp.tab : DEFAULT_INPUT_TAB;
12525
+ if (!byTab.has(name)) byTab.set(name, []);
12526
+ byTab.get(name).push(inp);
12527
+ }
12528
+ const names = [...byTab.keys()];
12529
+ if (names.includes(DEFAULT_INPUT_TAB)) {
12530
+ names.splice(names.indexOf(DEFAULT_INPUT_TAB), 1);
12531
+ names.unshift(DEFAULT_INPUT_TAB);
12532
+ }
12533
+ return names.map((name) => ({ name, inputs: byTab.get(name) }));
12534
+ }
11111
12535
  function groupInputs(inputs) {
11112
12536
  const order = [];
11113
12537
  const byGroup = /* @__PURE__ */ new Map();
@@ -11136,9 +12560,10 @@ function groupInputs(inputs) {
11136
12560
  });
11137
12561
  }
11138
12562
  var CHECK_SVG = iconAt("check", 12);
11139
- var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
12563
+ var CALENDAR_SVG = iconAt("calendar", 14);
12564
+ var CLOCK_SVG = iconAt("clock", 14);
11140
12565
  var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
11141
- var DIALOG_STYLE_REV = "10";
12566
+ var DIALOG_STYLE_REV = "25";
11142
12567
  function ensureDialogStyles() {
11143
12568
  if (typeof document === "undefined") return;
11144
12569
  const existing = document.getElementById(DIALOG_STYLE_ID);
@@ -11147,30 +12572,47 @@ function ensureDialogStyles() {
11147
12572
  s.id = DIALOG_STYLE_ID;
11148
12573
  s.dataset.rev = DIALOG_STYLE_REV;
11149
12574
  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;}
12575
+ .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;}
12576
+ .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);}
12577
+ .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);}
12578
+ .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;}
12579
+ .vela-ind-combo-chevron:hover{opacity:0.9;}
12580
+ .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;}
12581
+ .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
12582
+ .vela-ind-cal-title{flex:1;text-align:center;font-weight:600;font-size:14px;color:var(--vela-fg-bright);}
12583
+ .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;}
12584
+ .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
12585
+ .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
12586
+ .vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
12587
+ .vela-ind-cal-week span{line-height:20px;}
12588
+ .vela-ind-cal-blank{width:28px;height:28px;}
12589
+ .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;}
12590
+ .vela-ind-cal-day:hover{background:var(--vela-hover);}
12591
+ .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
12592
+ .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
12593
+ .vela-ind-dialog ::-webkit-scrollbar{width:9px;height:9px;}
11158
12594
  .vela-ind-dialog ::-webkit-scrollbar-thumb{background:var(--vela-scroll);border-radius:4px;border:2px solid transparent;background-clip:padding-box;}
11159
12595
  .vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
12596
+ .vela-ind-dialog ::-webkit-scrollbar-button{display:none;width:0;height:0;}
12597
+ .vela-ind-tab{transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
12598
+ .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
11160
12599
  .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
12600
  .vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
11162
12601
  .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
12602
  .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
12603
  .vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
11165
12604
  .vela-ind-close:hover{color:var(--vela-danger) !important;background-color:var(--vela-hover-strong);}
12605
+ .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;}
12606
+ .vela-ind-dlg-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
12607
+ .vela-ind-dlg-close svg{width:15px;height:15px;display:block;flex:none;}
11166
12608
  .vela-ind-fold{transition:border-color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
11167
12609
  .vela-ind-fold:hover{border-color:var(--vela-border-strong);opacity:0.9;}
11168
12610
  .vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
11169
12611
  .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;}`;
12612
+ .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;}
12613
+ .vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);border-color:var(--vela-fg-muted);}
12614
+ .vela-ind-btn-primary{border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
12615
+ .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
12616
  if (!existing) document.head.appendChild(s);
11175
12617
  }
11176
12618
  var TIMEFRAME_OPTIONS = [
@@ -11195,6 +12637,37 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
11195
12637
  const v = `${hh}:${mm}`;
11196
12638
  return { value: v, label: v };
11197
12639
  });
12640
+ var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
12641
+ var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
12642
+ function normalizeDateInput(raw) {
12643
+ const t = raw.trim();
12644
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
12645
+ if (!m) return null;
12646
+ const y = Number(m[1]);
12647
+ const mo = Number(m[2]);
12648
+ const d = Number(m[3]);
12649
+ const dt = new Date(y, mo - 1, d);
12650
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
12651
+ return isoDate(dt);
12652
+ }
12653
+ function parseIsoDate(raw) {
12654
+ const iso = normalizeDateInput(raw);
12655
+ if (!iso) return null;
12656
+ const [y, mo, d] = iso.split("-").map(Number);
12657
+ return new Date(y, mo - 1, d);
12658
+ }
12659
+ function isoDate(d) {
12660
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
12661
+ }
12662
+ function normalizeTimeInput(raw) {
12663
+ const t = raw.trim();
12664
+ const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
12665
+ if (!m) return null;
12666
+ const hh = Number(m[1]);
12667
+ const mm = Number(m[2]);
12668
+ if (hh > 23 || mm > 59) return null;
12669
+ return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
12670
+ }
11198
12671
  function sessionToTimes(session) {
11199
12672
  const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(session.trim());
11200
12673
  if (!m) return ["09:00", "16:00"];
@@ -14014,7 +15487,7 @@ var TOUCH_TAP_SLOP = 14;
14014
15487
  var DOUBLE_TAP_MS = 350;
14015
15488
  var DOUBLE_TAP_SLOP = 30;
14016
15489
  var TIME_SCALE_K = 4e-3;
14017
- var WHEEL_ZOOM_K = 25e-4;
15490
+ var WHEEL_ZOOM_K = 4e-3;
14018
15491
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
14019
15492
  if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
14020
15493
  return { logical: coords.xToLogical(cursorX), x: cursorX };
@@ -14068,6 +15541,8 @@ var InputController = class {
14068
15541
  // smoothed pointer velocity, px/ms
14069
15542
  this.middleDeleted = false;
14070
15543
  // the last middle press deleted a drawing (suppress autoscroll/paste)
15544
+ this.rightCancelled = false;
15545
+ // the last right press cancelled a placement (suppress the context menu)
14071
15546
  // Last cursor position over the element (NaN once it leaves) — lets a modifier
14072
15547
  // press/release re-shape the drawings preview with the pointer stationary.
14073
15548
  this.cursorX = NaN;
@@ -14092,6 +15567,14 @@ var InputController = class {
14092
15567
  this.onMiddleGuard = (e) => {
14093
15568
  if (e.button === 1 && this.middleDeleted) e.preventDefault();
14094
15569
  };
15570
+ /** A right press that cancelled a placement must not ALSO open the host's chart
15571
+ * context menu — swallow its companion event before it bubbles to the host. */
15572
+ this.onContextGuard = (e) => {
15573
+ if (!this.rightCancelled) return;
15574
+ this.rightCancelled = false;
15575
+ e.preventDefault();
15576
+ e.stopPropagation();
15577
+ };
14095
15578
  /** A modifier press/release with the pointer stationary: re-issue the last cursor
14096
15579
  * position so the placing ghost / drag preview reflects the new state immediately
14097
15580
  * (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
@@ -14108,6 +15591,11 @@ var InputController = class {
14108
15591
  if (this.middleDeleted) e.preventDefault();
14109
15592
  return;
14110
15593
  }
15594
+ if (e.button === 2) {
15595
+ this.rightCancelled = this.deps.drawingsCancelPlacement?.() ?? false;
15596
+ if (this.rightCancelled) e.preventDefault();
15597
+ return;
15598
+ }
14111
15599
  if (e.button !== 0) return;
14112
15600
  const { x, y } = this.local(e);
14113
15601
  if (e.pointerType === "touch") {
@@ -14312,6 +15800,7 @@ var InputController = class {
14312
15800
  el.addEventListener("wheel", this.onWheel, { passive: false });
14313
15801
  el.addEventListener("mousedown", this.onMiddleGuard);
14314
15802
  el.addEventListener("auxclick", this.onMiddleGuard);
15803
+ el.addEventListener("contextmenu", this.onContextGuard);
14315
15804
  const win = el.ownerDocument?.defaultView;
14316
15805
  win?.addEventListener("keydown", this.onModifier);
14317
15806
  win?.addEventListener("keyup", this.onModifier);
@@ -14333,6 +15822,7 @@ var InputController = class {
14333
15822
  el.removeEventListener("wheel", this.onWheel);
14334
15823
  el.removeEventListener("mousedown", this.onMiddleGuard);
14335
15824
  el.removeEventListener("auxclick", this.onMiddleGuard);
15825
+ el.removeEventListener("contextmenu", this.onContextGuard);
14336
15826
  this.cancelLongPress();
14337
15827
  this.touches.clear();
14338
15828
  this.el = null;
@@ -15284,6 +16774,12 @@ function extendEndpoints(x1, y1, x2, y2, extend, width, height) {
15284
16774
  const rx = right ? width + 2 : Math.max(x1, x2);
15285
16775
  return [lx, yAt(lx), rx, yAt(rx)];
15286
16776
  }
16777
+ function uprightLineAngle(x1, y1, x2, y2) {
16778
+ let a = Math.atan2(y2 - y1, x2 - x1);
16779
+ if (a > Math.PI / 2) a -= Math.PI;
16780
+ if (a < -Math.PI / 2) a += Math.PI;
16781
+ return a;
16782
+ }
15287
16783
  function parseColor2(c) {
15288
16784
  const s = c.trim();
15289
16785
  let m = /^#([0-9a-f]{3})$/i.exec(s);
@@ -16005,8 +17501,11 @@ var ChromeRenderer = class {
16005
17501
  }
16006
17502
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
16007
17503
  ctx.textBaseline = "middle";
16008
- if (coords.barCount === 0) return;
16009
17504
  const panes = scene.orderedPanes();
17505
+ if (coords.barCount === 0) {
17506
+ this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
17507
+ return;
17508
+ }
16010
17509
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
16011
17510
  for (const pane of panes) {
16012
17511
  if (pane.collapsed) continue;
@@ -16535,380 +18034,71 @@ function tzButtonLabel(zone) {
16535
18034
  return tzOffset(zone);
16536
18035
  }
16537
18036
 
16538
- // src/core/drawings/effectiveColor.ts
16539
- var VALID_FILL = VALID;
16540
- var INVALID_FILL = INVALID;
16541
- function effectiveFillColor(d, theme) {
16542
- if (d instanceof PatternDrawing) {
16543
- if (!d.fillTriangles().length) return null;
16544
- const valid = d.valid();
16545
- return d.style.fillColor ?? (valid == null ? d.style.lineColor : valid ? VALID_FILL : INVALID_FILL);
16546
- }
16547
- if (d instanceof CalloutBase) return d.style.fillColor ?? theme.background;
16548
- return d.style.fillColor ?? null;
16549
- }
16550
-
16551
- // src/renderers/native/drawings/colorPicker.ts
16552
- function splitColor(color) {
16553
- const s = String(color ?? "").trim();
16554
- let m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
16555
- if (m) return { hex6: `#${m[1].toLowerCase()}`, alpha: m[2] ? parseInt(m[2], 16) / 255 : 1 };
16556
- m = /^#([0-9a-f]{3})$/i.exec(s);
16557
- if (m) {
16558
- const h = m[1].toLowerCase();
16559
- return { hex6: `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`, alpha: 1 };
16560
- }
16561
- const rgba = /^rgba?\(([^)]+)\)/i.exec(s);
16562
- if (rgba) {
16563
- const p = rgba[1].split(",").map((v) => v.trim());
16564
- const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
16565
- const a = p[3] != null ? Math.max(0, Math.min(1, parseFloat(p[3]))) : 1;
16566
- return { hex6: `#${hx(parseInt(p[0] ?? "0", 10))}${hx(parseInt(p[1] ?? "0", 10))}${hx(parseInt(p[2] ?? "0", 10))}`, alpha: a };
16567
- }
16568
- return { hex6: DEFAULT_DRAWING_COLOR, alpha: 1 };
16569
- }
16570
- function combineColor(hex6, alpha) {
16571
- const a = Math.max(0, Math.min(1, alpha));
16572
- if (a >= 0.999) return hex6;
16573
- return `${hex6}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
16574
- }
16575
- function blendOver(fg, bg, alpha) {
16576
- const rgb = (c) => {
16577
- const n = parseInt(splitColor(c).hex6.slice(1), 16);
16578
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
16579
- };
16580
- const [fr, fgc, fb] = rgb(fg);
16581
- const [br, bgc, bb] = rgb(bg);
16582
- const a = Math.max(0, Math.min(1, alpha));
16583
- const h = (n) => Math.round(n).toString(16).padStart(2, "0");
16584
- return `#${h(fr * a + br * (1 - a))}${h(fgc * a + bgc * (1 - a))}${h(fb * a + bb * (1 - a))}`;
16585
- }
16586
- function hslHex(h, s, l) {
16587
- const sn = s / 100;
16588
- const ln = l / 100;
16589
- const k = (n) => (n + h / 30) % 12;
16590
- const a = sn * Math.min(ln, 1 - ln);
16591
- const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
16592
- const to = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
16593
- return `#${to(f(0))}${to(f(8))}${to(f(4))}`;
16594
- }
16595
- function buildPalette() {
16596
- const grays = [100, 90, 80, 68, 56, 45, 35, 25, 14, 0].map((l) => hslHex(0, 0, l));
16597
- const hues = [4, 28, 50, 96, 140, 174, 200, 224, 270, 322];
16598
- const levels = [
16599
- [82, 56],
16600
- // vivid
16601
- [58, 84],
16602
- // light pastel
16603
- [62, 74],
16604
- [66, 62],
16605
- [70, 50],
16606
- [64, 39],
16607
- [54, 29]
16608
- // dark
16609
- ];
16610
- return [grays, ...levels.map(([s, l]) => hues.map((h) => hslHex(h, s, l)))];
16611
- }
16612
- var PALETTE = buildPalette();
16613
- function transparencyChecker(size) {
16614
- return `repeating-conic-gradient(#9aa0a6 0% 25%, #d3d6da 0% 50%) 0 0 / ${size}px ${size}px`;
16615
- }
16616
- var CHECKER = transparencyChecker(10);
16617
- var recents = [ACCENT, BULLISH, BEARISH, WARNING, NEUTRAL];
16618
- function addRecent(hex6) {
16619
- const i = recents.findIndex((c) => c.toLowerCase() === hex6.toLowerCase());
16620
- if (i >= 0) recents.splice(i, 1);
16621
- recents.unshift(hex6);
16622
- if (recents.length > 10) recents.length = 10;
16623
- }
16624
- function buildColorPicker(color, theme, onChange) {
16625
- const parsed = splitColor(color);
16626
- let curHex = parsed.hex6;
16627
- let curAlpha = parsed.alpha;
16628
- const root = document.createElement("div");
16629
- root.style.cssText = "display:flex;flex-direction:column;gap:9px;width:236px;";
16630
- const grid = document.createElement("div");
16631
- grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
16632
- const swatches = [];
16633
- for (const row of PALETTE) {
16634
- for (const c of row) {
16635
- const sw = document.createElement("button");
16636
- sw.type = "button";
16637
- sw.dataset.c = c;
16638
- sw.style.cssText = `width:100%;aspect-ratio:1;border-radius:4px;border:none;cursor:pointer;background:${c};padding:0;`;
16639
- sw.addEventListener("click", (e) => {
16640
- e.stopPropagation();
16641
- pickHex(c);
16642
- });
16643
- grid.appendChild(sw);
16644
- swatches.push(sw);
16645
- }
16646
- }
16647
- const paintSelection = () => {
16648
- for (const sw of swatches) {
16649
- const on = (sw.dataset.c ?? "").toLowerCase() === curHex.toLowerCase();
16650
- sw.style.outline = on ? `2px solid ${theme.textColor}` : "none";
16651
- sw.style.outlineOffset = "2px";
16652
- sw.style.zIndex = on ? "1" : "";
16653
- sw.style.position = on ? "relative" : "";
16654
- }
16655
- };
16656
- const recentRow = document.createElement("div");
16657
- recentRow.style.cssText = "display:flex;align-items:center;gap:5px;flex-wrap:wrap;border-top:1px solid var(--vela-border);padding-top:9px;";
16658
- const renderRecents = () => {
16659
- recentRow.replaceChildren();
16660
- for (const c of recents) {
16661
- const sw = document.createElement("button");
16662
- sw.type = "button";
16663
- 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;`;
16664
- sw.addEventListener("click", (e) => {
16665
- e.stopPropagation();
16666
- pickHex(c);
16667
- });
16668
- recentRow.appendChild(sw);
16669
- }
16670
- const add = document.createElement("label");
16671
- 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;`;
16672
- add.textContent = "+";
16673
- const input = document.createElement("input");
16674
- input.type = "color";
16675
- input.value = curHex;
16676
- input.style.cssText = "position:absolute;inset:0;opacity:0;cursor:pointer;";
16677
- input.addEventListener("input", () => pickHex(input.value, true));
16678
- add.appendChild(input);
16679
- recentRow.appendChild(add);
16680
- };
16681
- const opLabel = document.createElement("div");
16682
- opLabel.textContent = "Opacity";
16683
- opLabel.style.cssText = `font:11px ${theme.fontFamily};color:var(--vela-fg-muted);`;
16684
- const opRow = document.createElement("div");
16685
- opRow.style.cssText = "display:flex;align-items:center;gap:10px;";
16686
- const track = document.createElement("div");
16687
- track.style.cssText = "flex:1;position:relative;height:13px;border-radius:7px;cursor:pointer;";
16688
- const knob = document.createElement("div");
16689
- 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;";
16690
- track.appendChild(knob);
16691
- const pctBox = document.createElement("div");
16692
- 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;`;
16693
- opRow.append(track, pctBox);
16694
- const paintOpacity = () => {
16695
- track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
16696
- knob.style.left = `${curAlpha * 100}%`;
16697
- pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
16698
- };
16699
- let dragging = false;
16700
- const onDrag = (clientX) => {
16701
- const r = track.getBoundingClientRect();
16702
- curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
16703
- paintOpacity();
16704
- emit();
16705
- };
16706
- track.addEventListener("pointerdown", (e) => {
16707
- e.stopPropagation();
16708
- dragging = true;
16709
- track.setPointerCapture(e.pointerId);
16710
- onDrag(e.clientX);
16711
- });
16712
- track.addEventListener("pointermove", (e) => {
16713
- if (dragging) onDrag(e.clientX);
16714
- });
16715
- track.addEventListener("pointerup", () => dragging = false);
16716
- function emit() {
16717
- onChange(combineColor(curHex, curAlpha));
16718
- }
16719
- function pickHex(hex, custom = false) {
16720
- curHex = splitColor(hex).hex6;
16721
- addRecent(curHex);
16722
- paintSelection();
16723
- paintOpacity();
16724
- if (custom) renderRecents();
16725
- emit();
16726
- }
16727
- renderRecents();
16728
- paintSelection();
16729
- paintOpacity();
16730
- root.append(grid, recentRow, opLabel, opRow);
16731
- return root;
16732
- }
16733
-
16734
- // src/renderers/native/chrome/ColorField.ts
16735
- var CHECKER2 = transparencyChecker(8);
16736
- var STYLE_ID2 = "vela-color-field";
16737
- function ensureStyles2() {
16738
- if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
16739
- const st = document.createElement("style");
16740
- st.id = STYLE_ID2;
16741
- st.textContent = `
16742
- .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;}
16743
- .vela-color-field:hover{border-color:var(--vela-fg-muted);}
16744
- .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);}
16745
- .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;}
16746
- `;
16747
- document.head.appendChild(st);
16748
- }
16749
- var openPopover = null;
16750
- function closeOpen() {
16751
- if (!openPopover) return;
16752
- document.removeEventListener("pointerdown", openPopover.onOutside, true);
16753
- window.removeEventListener("resize", openPopover.reflow, true);
16754
- openPopover.el.remove();
16755
- openPopover = null;
16756
- }
16757
- function place(pop, trigger) {
16758
- const r = trigger.getBoundingClientRect();
16759
- const w = pop.offsetWidth;
16760
- const h = pop.offsetHeight;
16761
- let left = r.right - w;
16762
- let top = r.bottom + 6;
16763
- if (left < 6) left = 6;
16764
- if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
16765
- if (top + h > window.innerHeight - 6) top = r.top - h - 6;
16766
- if (top < 6) top = 6;
16767
- pop.style.left = `${Math.round(left)}px`;
16768
- pop.style.top = `${Math.round(top)}px`;
16769
- }
16770
- function colorField(theme, getVal, onVal) {
16771
- ensureStyles2();
16772
- const trigger = document.createElement("button");
16773
- trigger.type = "button";
16774
- trigger.className = "vela-color-field";
16775
- const swatch = document.createElement("span");
16776
- swatch.className = "vela-color-field-swatch";
16777
- trigger.appendChild(swatch);
16778
- const paint = () => {
16779
- const v = getVal();
16780
- swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
16781
- };
16782
- paint();
16783
- trigger.addEventListener("vela-sync", paint);
16784
- trigger.addEventListener("click", (e) => {
16785
- e.stopPropagation();
16786
- if (openPopover && openPopover.trigger === trigger) {
16787
- closeOpen();
16788
- return;
16789
- }
16790
- closeOpen();
16791
- const pop = document.createElement("div");
16792
- pop.className = "vela-color-field-pop";
16793
- applyChromeTokens(pop, theme);
16794
- pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
16795
- pop.appendChild(
16796
- buildColorPicker(getVal(), theme, (val) => {
16797
- onVal(val);
16798
- paint();
16799
- })
16800
- );
16801
- document.body.appendChild(pop);
16802
- place(pop, trigger);
16803
- const onOutside = (ev) => {
16804
- const t = ev.target;
16805
- if (pop.contains(t) || trigger.contains(t)) return;
16806
- closeOpen();
16807
- };
16808
- const reflow = () => place(pop, trigger);
16809
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
16810
- window.addEventListener("resize", reflow, true);
16811
- openPopover = { el: pop, trigger, onOutside, reflow };
16812
- });
16813
- return trigger;
16814
- }
16815
- function closeColorPopover() {
16816
- closeOpen();
16817
- }
16818
-
16819
18037
  // src/renderers/native/chrome/WidthField.ts
16820
18038
  var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
16821
- var STYLE_ID3 = "vela-width-field";
16822
- function ensureStyles3() {
16823
- if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
16824
- const st = document.createElement("style");
16825
- st.id = STYLE_ID3;
16826
- st.textContent = `
18039
+ var STYLE_ID2 = "vela-width-field";
18040
+ var WIDTH_CSS = `
16827
18041
  .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;}
16828
18042
  .vela-width-field:hover{border-color:var(--vela-fg-muted);}
16829
- .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);}
18043
+ .vela-width-field-caret{display:flex;color:var(--vela-fg-muted);}
18044
+ .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);}
16830
18045
  .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;}
16831
18046
  .vela-width-field-item:hover{background:var(--vela-hover-strong);}
16832
18047
  .vela-width-field-item[data-active='1']{background:var(--vela-active);}
16833
18048
  `;
16834
- document.head.appendChild(st);
16835
- }
16836
18049
  function lineGlyph(width) {
16837
18050
  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>`;
16838
18051
  }
16839
- 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>`;
16840
- var openPopover2 = null;
16841
- function closeOpen2() {
16842
- if (!openPopover2) return;
16843
- document.removeEventListener("pointerdown", openPopover2.onOutside, true);
16844
- window.removeEventListener("resize", openPopover2.reflow, true);
16845
- openPopover2.el.remove();
16846
- openPopover2 = null;
16847
- }
16848
- function place2(pop, trigger) {
16849
- const r = trigger.getBoundingClientRect();
16850
- const w = pop.offsetWidth;
16851
- const h = pop.offsetHeight;
16852
- let left = r.right - w;
16853
- let top = r.bottom + 6;
16854
- if (left < 6) left = 6;
16855
- if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
16856
- if (top + h > window.innerHeight - 6) top = r.top - h - 6;
16857
- if (top < 6) top = 6;
16858
- pop.style.left = `${Math.round(left)}px`;
16859
- pop.style.top = `${Math.round(top)}px`;
16860
- }
18052
+ 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>`;
16861
18053
  function widthField(theme, getVal, onVal) {
16862
- ensureStyles3();
18054
+ injectStyles(STYLE_ID2, WIDTH_CSS, document);
16863
18055
  const trigger = document.createElement("button");
16864
18056
  trigger.type = "button";
16865
18057
  trigger.className = "vela-width-field";
16866
18058
  const paint = () => {
16867
- trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
18059
+ trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span class="vela-width-field-caret">${CHEVRON}</span>`;
16868
18060
  };
16869
18061
  paint();
18062
+ trigger.addEventListener("pointerdown", (e) => {
18063
+ e.preventDefault();
18064
+ trigger.focus({ preventScroll: true });
18065
+ });
16870
18066
  trigger.addEventListener("click", (e) => {
16871
18067
  e.stopPropagation();
16872
- if (openPopover2 && openPopover2.trigger === trigger) {
16873
- closeOpen2();
18068
+ if (openPopoverTrigger() === trigger) {
18069
+ closeOpenPopovers();
16874
18070
  return;
16875
18071
  }
16876
- closeOpen2();
16877
- const pop = document.createElement("div");
16878
- pop.className = "vela-width-field-pop";
16879
- applyChromeTokens(pop, theme);
16880
- pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
16881
- pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
16882
- for (const w of WIDTH_FIELD_OPTIONS) {
16883
- const item = document.createElement("button");
16884
- item.type = "button";
16885
- item.className = "vela-width-field-item";
16886
- item.dataset.active = w === getVal() ? "1" : "0";
16887
- item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
16888
- item.addEventListener("click", (ev) => {
16889
- ev.stopPropagation();
16890
- closeOpen2();
16891
- onVal(w);
16892
- paint();
16893
- });
16894
- pop.appendChild(item);
16895
- }
16896
- document.body.appendChild(pop);
16897
- place2(pop, trigger);
16898
- const onOutside = (ev) => {
16899
- const t = ev.target;
16900
- if (pop.contains(t) || trigger.contains(t)) return;
16901
- closeOpen2();
16902
- };
16903
- const reflow = () => place2(pop, trigger);
16904
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
16905
- window.addEventListener("resize", reflow, true);
16906
- openPopover2 = { el: pop, trigger, onOutside, reflow };
18072
+ const pop = new Popover({
18073
+ trigger,
18074
+ theme,
18075
+ align: "end",
18076
+ gap: 6,
18077
+ className: "vela-width-field-pop",
18078
+ content: (el) => {
18079
+ el.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
18080
+ for (const w of WIDTH_FIELD_OPTIONS) {
18081
+ const item = document.createElement("button");
18082
+ item.type = "button";
18083
+ item.className = "vela-width-field-item";
18084
+ item.dataset.active = w === getVal() ? "1" : "0";
18085
+ item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
18086
+ item.addEventListener("click", (ev) => {
18087
+ ev.stopPropagation();
18088
+ pop.hide();
18089
+ onVal(w);
18090
+ paint();
18091
+ });
18092
+ el.appendChild(item);
18093
+ }
18094
+ }
18095
+ });
18096
+ pop.show();
16907
18097
  });
16908
18098
  return trigger;
16909
18099
  }
16910
18100
  function closeWidthPopover() {
16911
- closeOpen2();
18101
+ closeOpenPopovers();
16912
18102
  }
16913
18103
 
16914
18104
  // src/renderers/native/chrome/SettingsDialog.ts
@@ -16923,37 +18113,24 @@ function styleLabel(id) {
16923
18113
  return chartType(id)?.label ?? BUILTIN_STYLE_LABELS[id] ?? id;
16924
18114
  }
16925
18115
  var SD_STYLE_ID = "vela-settings-controls";
18116
+ var SD_STYLE_REV = "2";
16926
18117
  var SETTINGS_SURFACE = "var(--vela-surface)";
16927
18118
  var SETTINGS_BORDER = "var(--vela-border)";
16928
18119
  var SETTINGS_TEXT = "var(--vela-fg)";
16929
18120
  function ensureControlStyles() {
16930
- if (typeof document === "undefined" || document.getElementById(SD_STYLE_ID)) return;
16931
- const st = document.createElement("style");
18121
+ if (typeof document === "undefined") return;
18122
+ const existing = document.getElementById(SD_STYLE_ID);
18123
+ if (existing?.dataset.rev === SD_STYLE_REV) return;
18124
+ const st = existing ?? document.createElement("style");
16932
18125
  st.id = SD_STYLE_ID;
18126
+ st.dataset.rev = SD_STYLE_REV;
16933
18127
  st.textContent = `
16934
- .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;}
16935
- .vela-sd-check:hover{border-color:var(--vela-fg-muted);}
16936
- .vela-sd-check.on{background:var(--vela-selected-bg);border-color:var(--vela-selected-bg);color:var(--vela-selected-fg);}
16937
- .vela-sd-check svg{display:block;}
16938
- .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;}
16939
- .vela-sd-select:hover,.vela-sd-number:hover{border-color:var(--vela-fg-muted);}
16940
- /* Custom caret: the native one hugs the right border; ours gets 8px of air. (A data URI
16941
- can't read currentColor, so it uses the shared muted-gray ink, legible on both themes.) */
16942
- .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;}
16943
- .vela-sd-number{width:64px;}
16944
- .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;}
16945
- .vela-sd-color::-webkit-color-swatch-wrapper{padding:2px;}
16946
- .vela-sd-color::-webkit-color-swatch{border:none;border-radius:2px;}
16947
18128
  .vela-sd-pane::-webkit-scrollbar{width:8px;}
16948
18129
  .vela-sd-pane::-webkit-scrollbar-thumb{background:var(--vela-scroll);border-radius:var(--vela-radius-sm);border:2px solid transparent;background-clip:padding-box;}
16949
18130
  .vela-sd-pane::-webkit-scrollbar-track{background:transparent;}
16950
- .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;}
16951
- .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;}
16952
- .vela-sd-toggle.on{background:var(--vela-active);border-color:var(--vela-selected-bg);}
16953
- .vela-sd-toggle.on::after{transform:translateX(16px);background:var(--vela-selected-bg);}
16954
18131
  /* Tab rail / footer button / header close: base styles live HERE, not inline on the
16955
18132
  elements \u2014 inline declarations always beat stylesheet :hover rules, which is exactly
16956
- what killed these hovers before. Active tab state is the .on class (like .vela-sd-check),
18133
+ what killed these hovers before. Active tab state is the .on class,
16957
18134
  hover fills follow the app convention (--vela-hover + --vela-fg-bright, fast transition). */
16958
18135
  .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;}
16959
18136
  .vela-sd-tab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
@@ -17012,13 +18189,13 @@ function ensureControlStyles() {
17012
18189
  .vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
17013
18190
  .vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
17014
18191
  .vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
17015
- .vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
17016
- .vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
18192
+ .vela-sd-mobile .vela-switch{width:22px;height:22px;}
18193
+ .vela-sd-mobile .vela-select[data-size='sm'] .vela-select-trigger,.vela-sd-mobile .vela-num[data-size='sm'] input{height:34px;}
17017
18194
  .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
17018
18195
  .vela-sd-mobile .vela-sd-btn{height:38px;}
17019
18196
  .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
17020
18197
  `;
17021
- document.head.appendChild(st);
18198
+ if (!existing) document.head.appendChild(st);
17022
18199
  }
17023
18200
  var SettingsDialog = class {
17024
18201
  constructor(container, theme) {
@@ -17334,15 +18511,15 @@ var SettingsDialog = class {
17334
18511
  railScrim = document.createElement("div");
17335
18512
  railScrim.className = "vela-sd-railscrim";
17336
18513
  railScrim.addEventListener("click", () => toggleRail?.());
17337
- toggleRail = (open) => {
17338
- const on = open ?? !rail.classList.contains("open");
18514
+ toggleRail = (open2) => {
18515
+ const on = open2 ?? !rail.classList.contains("open");
17339
18516
  rail.classList.toggle("open", on);
17340
18517
  railScrim?.classList.toggle("open", on);
17341
18518
  };
17342
18519
  }
17343
18520
  const paneHost = document.createElement("div");
17344
18521
  paneHost.className = "vela-sd-pane";
17345
- 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;";
18522
+ 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;";
17346
18523
  const panes = [];
17347
18524
  let current = null;
17348
18525
  for (const child of [...body.children]) {
@@ -17408,7 +18585,7 @@ var SettingsDialog = class {
17408
18585
  }
17409
18586
  }
17410
18587
  close() {
17411
- closeColorPopover();
18588
+ closeOpenPopovers();
17412
18589
  closeWidthPopover();
17413
18590
  this.root?.remove();
17414
18591
  this.root = null;
@@ -17563,50 +18740,41 @@ var SettingsDialog = class {
17563
18740
  return wf;
17564
18741
  }
17565
18742
  if (c.kind === "select") {
17566
- const sel = document.createElement("select");
17567
- sel.className = "vela-sd-select";
17568
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
17569
- sel.title = c.label;
17570
18743
  const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
17571
- for (const [val, lbl] of normalizeSelectOptions(c.options)) {
17572
- const o = document.createElement("option");
17573
- o.value = val;
17574
- o.textContent = lbl;
17575
- if (val === current2) o.selected = true;
17576
- sel.appendChild(o);
17577
- }
17578
- sel.addEventListener("change", () => put(c.key, sel.value));
17579
- sel.addEventListener("vela-sync", () => {
17580
- sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
17581
- });
17582
- return sel;
17583
- }
17584
- const ni = document.createElement("input");
17585
- ni.type = "number";
17586
- ni.className = "vela-sd-number";
17587
- if (compact) ni.style.width = "56px";
17588
- if (c.min !== void 0) ni.min = String(c.min);
17589
- if (c.max !== void 0) ni.max = String(c.max);
17590
- ni.step = String(c.step ?? 1);
17591
- ni.title = c.label;
17592
- const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
17593
- if (c.placeholder !== void 0) {
17594
- ni.placeholder = c.placeholder;
17595
- if (current !== c.defval) ni.value = String(current);
17596
- ni.addEventListener("change", () => {
17597
- const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
17598
- const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
17599
- ni.value = v === c.defval ? "" : String(v);
17600
- put(c.key, v);
18744
+ const sel = new Select({
18745
+ options: normalizeSelectOptions(c.options).map(([value, label]) => ({ value, label })),
18746
+ value: current2,
18747
+ size: "sm",
18748
+ theme: this.theme,
18749
+ onChange: (v) => put(c.key, v)
17601
18750
  });
17602
- } else {
17603
- ni.value = String(current);
17604
- ni.addEventListener("input", () => {
17605
- const v = Number(ni.value);
17606
- if (Number.isFinite(v)) put(c.key, v);
18751
+ sel.el.title = c.label;
18752
+ sel.el.addEventListener("vela-sync", () => {
18753
+ sel.setValue(typeof bag[c.key] === "string" ? bag[c.key] : c.defval);
17607
18754
  });
18755
+ return sel.el;
17608
18756
  }
17609
- return ni;
18757
+ const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
18758
+ const ni = new NumberInput({
18759
+ value: current,
18760
+ min: c.min,
18761
+ max: c.max,
18762
+ step: c.step ?? 1,
18763
+ size: "sm",
18764
+ commit: "live",
18765
+ clamp: c.placeholder !== void 0,
18766
+ steppers: false,
18767
+ compact,
18768
+ title: c.label,
18769
+ placeholder: c.placeholder,
18770
+ emptyValue: c.placeholder !== void 0 ? c.defval : void 0,
18771
+ onChange: (v) => put(c.key, v)
18772
+ });
18773
+ ni.el.addEventListener("vela-sync", () => {
18774
+ const next = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
18775
+ ni.setValue(next);
18776
+ });
18777
+ return ni.el;
17610
18778
  }
17611
18779
  /**
17612
18780
  * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
@@ -17887,28 +19055,27 @@ var SettingsDialog = class {
17887
19055
  * only one visible at a time; see `typeRow`). */
17888
19056
  toggleRow(label, value, onToggle, controls, get) {
17889
19057
  const wrap = document.createElement("div");
17890
- const cb = document.createElement("button");
17891
- cb.type = "button";
17892
- cb.className = "vela-sd-check" + (value ? " on" : "");
17893
- cb.innerHTML = iconAt("check", 11);
17894
- let checked = value;
17895
- const cbToggle = () => {
17896
- checked = !checked;
17897
- cb.classList.toggle("on", checked);
17898
- return checked;
17899
- };
19058
+ const sw = new Switch({ checked: value, size: "sm", onChange: (v) => {
19059
+ onToggle(v);
19060
+ if (controls.length > 0) syncDim(v);
19061
+ } });
17900
19062
  const lbl = document.createElement("span");
17901
19063
  lbl.textContent = label;
17902
19064
  lbl.style.cssText = "opacity:0.85;white-space:nowrap;";
19065
+ const syncDim = (on) => {
19066
+ for (const c of controls) {
19067
+ if (c.dataset.sdSelfGated === "1") continue;
19068
+ c.style.opacity = on ? "1" : "0.4";
19069
+ c.style.pointerEvents = on ? "" : "none";
19070
+ }
19071
+ };
17903
19072
  if (controls.length === 0) {
17904
19073
  wrap.className = "vela-sd-bool";
17905
19074
  wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
17906
- wrap.append(cb, lbl);
17907
- cb.addEventListener("click", () => onToggle(cbToggle()));
19075
+ wrap.append(sw.el, lbl);
17908
19076
  if (get) {
17909
19077
  wrap.addEventListener("vela-sync", () => {
17910
- checked = get();
17911
- cb.classList.toggle("on", checked);
19078
+ sw.setChecked(get());
17912
19079
  });
17913
19080
  }
17914
19081
  return wrap;
@@ -17916,28 +19083,16 @@ var SettingsDialog = class {
17916
19083
  wrap.className = "vela-sd-row";
17917
19084
  const left = document.createElement("div");
17918
19085
  left.style.cssText = "display:flex;align-items:center;gap:8px;";
17919
- left.append(cb, lbl);
19086
+ left.append(sw.el, lbl);
17920
19087
  const box = document.createElement("div");
17921
19088
  box.style.cssText = "display:flex;align-items:center;gap:6px;";
17922
19089
  for (const c of controls) box.appendChild(c);
17923
- const syncDim = (on) => {
17924
- for (const c of controls) {
17925
- if (c.dataset.sdSelfGated === "1") continue;
17926
- c.style.opacity = on ? "1" : "0.4";
17927
- c.style.pointerEvents = on ? "" : "none";
17928
- }
17929
- };
17930
19090
  syncDim(value);
17931
- cb.addEventListener("click", () => {
17932
- const v = cbToggle();
17933
- onToggle(v);
17934
- syncDim(v);
17935
- });
17936
19091
  if (get) {
17937
19092
  wrap.addEventListener("vela-sync", () => {
17938
- checked = get();
17939
- cb.classList.toggle("on", checked);
17940
- syncDim(checked);
19093
+ const v = get();
19094
+ sw.setChecked(v);
19095
+ syncDim(v);
17941
19096
  });
17942
19097
  }
17943
19098
  wrap.append(left, box);
@@ -17945,52 +19100,23 @@ var SettingsDialog = class {
17945
19100
  }
17946
19101
  numberRow(label, value, min, max, step, onChange) {
17947
19102
  const { wrap } = this.row(label);
17948
- const ni = document.createElement("input");
17949
- ni.type = "number";
17950
- ni.value = String(value);
17951
- ni.min = String(min);
17952
- ni.max = String(max);
17953
- ni.step = String(step);
17954
- ni.className = "vela-sd-number";
17955
- ni.addEventListener("input", () => {
17956
- const n = Number(ni.value);
17957
- if (Number.isFinite(n)) onChange(n);
17958
- });
17959
- wrap.appendChild(ni);
19103
+ wrap.appendChild(new NumberInput({ value, min, max, step, size: "sm", commit: "live", onChange }).el);
17960
19104
  return wrap;
17961
19105
  }
17962
19106
  /** A dropdown whose option values differ from their display labels. */
17963
19107
  selectRowLabeled(label, value, options, onChange) {
17964
19108
  const { wrap } = this.row(label);
17965
- const sel = document.createElement("select");
17966
- sel.className = "vela-sd-select";
17967
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
17968
- for (const [val, lbl] of options) {
17969
- const o = document.createElement("option");
17970
- o.value = val;
17971
- o.textContent = lbl;
17972
- if (val === value) o.selected = true;
17973
- sel.appendChild(o);
17974
- }
17975
- sel.addEventListener("change", () => onChange(sel.value));
17976
- wrap.appendChild(sel);
19109
+ wrap.appendChild(new Select({
19110
+ options: options.map(([v, l]) => ({ value: v, label: l })),
19111
+ value,
19112
+ size: "sm",
19113
+ theme: this.theme,
19114
+ onChange
19115
+ }).el);
17977
19116
  return wrap;
17978
19117
  }
17979
19118
  selectRow(label, value, options, onChange) {
17980
- const { wrap } = this.row(label);
17981
- const sel = document.createElement("select");
17982
- sel.className = "vela-sd-select";
17983
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
17984
- for (const opt of options) {
17985
- const o = document.createElement("option");
17986
- o.value = opt;
17987
- o.textContent = opt;
17988
- if (opt === value) o.selected = true;
17989
- sel.appendChild(o);
17990
- }
17991
- sel.addEventListener("change", () => onChange(sel.value));
17992
- wrap.appendChild(sel);
17993
- return wrap;
19119
+ return this.selectRowLabeled(label, value, options.map((o) => [o, o]), onChange);
17994
19120
  }
17995
19121
  /** Footer with the full config as JSON — the export/import (templating) surface. */
17996
19122
  footer(_config) {
@@ -18015,6 +19141,19 @@ function timezoneOptions(current) {
18015
19141
  return [[normalized, tzMenuLabel(normalized, normalized)], ...options];
18016
19142
  }
18017
19143
 
19144
+ // src/core/drawings/effectiveColor.ts
19145
+ var VALID_FILL = VALID;
19146
+ var INVALID_FILL = INVALID;
19147
+ function effectiveFillColor(d, theme) {
19148
+ if (d instanceof PatternDrawing) {
19149
+ if (!d.fillTriangles().length) return null;
19150
+ const valid = d.valid();
19151
+ return d.style.fillColor ?? (valid == null ? d.style.lineColor : valid ? VALID_FILL : INVALID_FILL);
19152
+ }
19153
+ if (d instanceof CalloutBase) return d.style.fillColor ?? theme.background;
19154
+ return d.style.fillColor ?? null;
19155
+ }
19156
+
18018
19157
  // src/renderers/native/drawings/DrawingPainter.ts
18019
19158
  var HANDLE_RADIUS = 4.5;
18020
19159
  var HANDLE_BORDER = DEFAULT_DRAWING_COLOR;
@@ -18381,13 +19520,19 @@ var DrawingPainter = class {
18381
19520
  }
18382
19521
  this.paintLabel(ctx, d, proj, theme);
18383
19522
  }
18384
- /** Draw a drawing's optional text label at a type-appropriate anchor (multi-line aware). */
19523
+ /** Draw a drawing's optional text label at a type-appropriate anchor (multi-line aware).
19524
+ * Two-point line tools rotate the glyphs to follow the segment, kept upright. */
18385
19525
  paintLabel(ctx, d, proj, theme) {
18386
19526
  const text = d.text;
18387
19527
  if (!text || !text.value || d.id === this.targets.mutedLabel) return;
18388
19528
  const layout = labelLayout(d, proj);
18389
19529
  if (!layout) return;
18390
19530
  const fs = namedFontSize(text.size);
19531
+ ctx.save();
19532
+ if (layout.rotate) {
19533
+ ctx.translate(layout.rotate.cx, layout.rotate.cy);
19534
+ ctx.rotate(layout.rotate.angle);
19535
+ }
18391
19536
  ctx.font = `${text.bold ? "bold " : ""}${text.italic ? "italic " : ""}${fs}px ${theme.fontFamily}`;
18392
19537
  ctx.textBaseline = "top";
18393
19538
  ctx.textAlign = layout.align;
@@ -18395,8 +19540,8 @@ var DrawingPainter = class {
18395
19540
  const lh = labelLineHeight(fs);
18396
19541
  const lines = text.value.split("\n");
18397
19542
  lines.forEach((line, i) => ctx.fillText(line, layout.x, layout.top + i * lh));
18398
- ctx.textAlign = "left";
18399
19543
  if (d.type === "text") this.paintTextFrame(ctx, d.id, layout.x, layout.top, lines, fs, theme);
19544
+ ctx.restore();
18400
19545
  }
18401
19546
  /** Frame a plain text label while it is the target, so the text reads as a clickable object even
18402
19547
  * though it has no shape of its own: a firm frame once selected, a fainter one under the cursor.
@@ -19339,7 +20484,14 @@ function labelLayout(d, proj) {
19339
20484
  case "infoline":
19340
20485
  case "trendangle": {
19341
20486
  if (pts.length < 2) return null;
19342
- return { x: (pts[0][0] + pts[1][0]) / 2, top: (pts[0][1] + pts[1][1]) / 2 - 6 - lh * lines, align: "center" };
20487
+ const [x1, y1] = pts[0];
20488
+ const [x2, y2] = pts[1];
20489
+ return {
20490
+ x: 0,
20491
+ top: -6 - lh * lines,
20492
+ align: "center",
20493
+ rotate: { angle: uprightLineAngle(x1, y1, x2, y2), cx: (x1 + x2) / 2, cy: (y1 + y2) / 2 }
20494
+ };
19343
20495
  }
19344
20496
  case "box": {
19345
20497
  if (pts.length < 2) return null;
@@ -19688,11 +20840,11 @@ function applyFree(anchor, pt, free) {
19688
20840
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
19689
20841
  var BTN = 30;
19690
20842
  var ICON = 21;
19691
- var STYLE_ID4 = "vela-drawing-popup-styles";
19692
- function ensureStyles4() {
19693
- if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
20843
+ var STYLE_ID3 = "vela-drawing-popup-styles";
20844
+ function ensureStyles2() {
20845
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
19694
20846
  const s = document.createElement("style");
19695
- s.id = STYLE_ID4;
20847
+ s.id = STYLE_ID3;
19696
20848
  s.textContent = `
19697
20849
  .vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
19698
20850
  .vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -19726,17 +20878,14 @@ var DrawingSettingsPopup = class {
19726
20878
  this.textPanel = null;
19727
20879
  this.levelsPanel = null;
19728
20880
  this.colorPop = null;
19729
- // floating RGB + opacity picker for a color swatch
19730
20881
  this.colorOwner = null;
19731
- // the swatch the open color popover belongs to (for toggle)
19732
- this.menuEl = null;
19733
- // floating option list for a dropdown control
20882
+ this.menuPop = null;
19734
20883
  this.menuOwner = null;
19735
20884
  this.onClose = null;
19736
20885
  this.onOutside = (e) => {
19737
20886
  const target = e.target;
19738
- if (this.colorPop?.contains(target)) return;
19739
- if (this.menuEl?.contains(target)) return;
20887
+ if (this.colorPop?.el.contains(target)) return;
20888
+ if (this.menuPop?.el.contains(target)) return;
19740
20889
  if (this.el && !this.el.contains(target)) {
19741
20890
  const cb = this.onClose;
19742
20891
  this.close();
@@ -19755,13 +20904,13 @@ var DrawingSettingsPopup = class {
19755
20904
  * picker / dropdown menu), which live outside `el`. */
19756
20905
  contains(node) {
19757
20906
  if (!node) return false;
19758
- return this.el?.contains(node) === true || this.colorPop?.contains(node) === true || this.menuEl?.contains(node) === true;
20907
+ return this.el?.contains(node) === true || this.colorPop?.el.contains(node) === true || this.menuPop?.el.contains(node) === true;
19759
20908
  }
19760
20909
  /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
19761
20910
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
19762
20911
  open(drawing, anchor, actions, onClose) {
19763
20912
  this.close();
19764
- ensureStyles4();
20913
+ ensureStyles2();
19765
20914
  this.onClose = onClose ?? null;
19766
20915
  const t = this.theme;
19767
20916
  const schema = drawing.schema();
@@ -19772,8 +20921,7 @@ var DrawingSettingsPopup = class {
19772
20921
  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;`;
19773
20922
  applyChromeTokens(el, t);
19774
20923
  el.addEventListener("pointerdown", () => {
19775
- this.closeColorPopover();
19776
- this.closeMenu();
20924
+ closeOpenPopovers();
19777
20925
  });
19778
20926
  const bar = document.createElement("div");
19779
20927
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
@@ -20510,49 +21658,76 @@ var DrawingSettingsPopup = class {
20510
21658
  });
20511
21659
  return b;
20512
21660
  }
21661
+ /** Host-anchored floating shell (stays inside the chart). Content is filled after
21662
+ * construction so `fill` can close over the live `Popover` without hitting TDZ. */
21663
+ hostFloat(anchor, opts) {
21664
+ const t = this.theme;
21665
+ const pop = new Popover({
21666
+ trigger: anchor,
21667
+ host: this.host,
21668
+ theme: t,
21669
+ position: "absolute",
21670
+ boundary: this.host,
21671
+ boundaryInset: 4,
21672
+ viewportInset: 0,
21673
+ gap: 6,
21674
+ align: opts.align ?? "start",
21675
+ zIndex: opts.zIndex,
21676
+ onClose: () => {
21677
+ if (this.menuPop === pop) {
21678
+ this.menuPop = null;
21679
+ this.menuOwner = null;
21680
+ }
21681
+ if (this.colorPop === pop) {
21682
+ this.colorPop = null;
21683
+ this.colorOwner = null;
21684
+ }
21685
+ }
21686
+ });
21687
+ const el = pop.el;
21688
+ el.style.background = t.background;
21689
+ el.style.border = "1px solid var(--vela-border)";
21690
+ el.style.borderRadius = "var(--vela-radius-lg)";
21691
+ el.style.boxShadow = "var(--vela-shadow)";
21692
+ el.style.padding = opts.padding;
21693
+ el.style.display = "flex";
21694
+ el.style.flexDirection = "column";
21695
+ el.style.gap = "1px";
21696
+ el.style.color = t.textColor;
21697
+ el.style.font = `var(--vela-font-size-md) ${t.fontFamily}`;
21698
+ el.style.pointerEvents = "auto";
21699
+ opts.fill(el, pop);
21700
+ pop.show();
21701
+ return pop;
21702
+ }
20513
21703
  /** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
20514
21704
  openActionMenu(anchor, rows) {
20515
- this.closeMenu();
20516
- this.closeColorPopover();
20517
- const t = this.theme;
20518
- const menu2 = document.createElement("div");
20519
- menu2.className = "vela-dpop";
20520
- 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};`;
20521
- applyChromeTokens(menu2, t);
20522
- menu2.addEventListener("pointerdown", (e) => e.stopPropagation());
20523
- for (const row of rows) {
20524
- const item = document.createElement("button");
20525
- item.type = "button";
20526
- item.className = "vela-dpop-item";
20527
- 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;";
20528
- const ic = document.createElement("span");
20529
- ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
20530
- ic.innerHTML = sized(row.icon, 18);
20531
- const tx = document.createElement("span");
20532
- tx.textContent = row.label;
20533
- tx.style.cssText = "flex:1;";
20534
- item.append(ic, tx);
20535
- item.addEventListener("click", (e) => {
20536
- e.stopPropagation();
20537
- this.closeMenu();
20538
- row.onClick();
20539
- });
20540
- menu2.appendChild(item);
20541
- }
20542
- this.host.appendChild(menu2);
20543
- const ar = anchor.getBoundingClientRect();
20544
- const hr = this.host.getBoundingClientRect();
20545
- const w = menu2.offsetWidth;
20546
- const h = menu2.offsetHeight;
20547
- let left = ar.right - hr.left - w;
20548
- let top = ar.bottom - hr.top + 6;
20549
- if (left + w > hr.width - 4) left = hr.width - w - 4;
20550
- if (left < 4) left = 4;
20551
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
20552
- if (top < 4) top = 4;
20553
- menu2.style.left = `${Math.round(left)}px`;
20554
- menu2.style.top = `${Math.round(top)}px`;
20555
- this.menuEl = menu2;
21705
+ this.menuPop = this.hostFloat(anchor, {
21706
+ align: "end",
21707
+ zIndex: 26,
21708
+ padding: "4px",
21709
+ fill: (menu2, pop) => {
21710
+ for (const row of rows) {
21711
+ const item = document.createElement("button");
21712
+ item.type = "button";
21713
+ item.className = "vela-dpop-item";
21714
+ 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;";
21715
+ const ic = document.createElement("span");
21716
+ ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
21717
+ ic.innerHTML = sized(row.icon, 18);
21718
+ const tx = document.createElement("span");
21719
+ tx.textContent = row.label;
21720
+ tx.style.cssText = "flex:1;";
21721
+ item.append(ic, tx);
21722
+ item.addEventListener("click", (e) => {
21723
+ e.stopPropagation();
21724
+ pop.hide();
21725
+ row.onClick();
21726
+ });
21727
+ menu2.appendChild(item);
21728
+ }
21729
+ }
21730
+ });
20556
21731
  this.menuOwner = anchor;
20557
21732
  }
20558
21733
  /** The floating hover-label. Sits just above or below the toolbar depending on where it floats,
@@ -20682,60 +21857,40 @@ var DrawingSettingsPopup = class {
20682
21857
  /** The floating option list opened by a {@link dropdown} trigger. Each row shows the option's
20683
21858
  * glyph (and text label when one is supplied); the current value is highlighted. */
20684
21859
  openMenu(anchor, values, current, render, label, onPick) {
20685
- this.closeMenu();
20686
- this.closeColorPopover();
20687
- const t = this.theme;
20688
- const menu2 = document.createElement("div");
20689
- menu2.className = "vela-dpop";
20690
- 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};`;
20691
- applyChromeTokens(menu2, t);
20692
- menu2.addEventListener("pointerdown", (e) => e.stopPropagation());
20693
- for (const v of values) {
20694
- const active = v === current;
20695
- const item = document.createElement("button");
20696
- item.type = "button";
20697
- item.className = "vela-dpop-item";
20698
- item.dataset.active = active ? "1" : "0";
20699
- 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;`;
20700
- const ic = document.createElement("span");
20701
- ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
20702
- ic.innerHTML = sized(render(v), 18);
20703
- item.appendChild(ic);
20704
- if (label) {
20705
- const tx = document.createElement("span");
20706
- tx.textContent = label(v);
20707
- tx.style.cssText = "flex:1;font-variant-numeric:tabular-nums;";
20708
- item.appendChild(tx);
21860
+ this.menuPop = this.hostFloat(anchor, {
21861
+ zIndex: 26,
21862
+ padding: "4px",
21863
+ fill: (menu2, pop) => {
21864
+ for (const v of values) {
21865
+ const active = v === current;
21866
+ const item = document.createElement("button");
21867
+ item.type = "button";
21868
+ item.className = "vela-dpop-item";
21869
+ item.dataset.active = active ? "1" : "0";
21870
+ 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;`;
21871
+ const ic = document.createElement("span");
21872
+ ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
21873
+ ic.innerHTML = sized(render(v), 18);
21874
+ item.appendChild(ic);
21875
+ if (label) {
21876
+ const tx = document.createElement("span");
21877
+ tx.textContent = label(v);
21878
+ tx.style.cssText = "flex:1;font-variant-numeric:tabular-nums;";
21879
+ item.appendChild(tx);
21880
+ }
21881
+ item.addEventListener("click", (e) => {
21882
+ e.stopPropagation();
21883
+ pop.hide();
21884
+ onPick(v);
21885
+ });
21886
+ menu2.appendChild(item);
21887
+ }
20709
21888
  }
20710
- item.addEventListener("click", (e) => {
20711
- e.stopPropagation();
20712
- this.closeMenu();
20713
- onPick(v);
20714
- });
20715
- menu2.appendChild(item);
20716
- }
20717
- this.host.appendChild(menu2);
20718
- const ar = anchor.getBoundingClientRect();
20719
- const hr = this.host.getBoundingClientRect();
20720
- const w = menu2.offsetWidth;
20721
- const h = menu2.offsetHeight;
20722
- let left = ar.left - hr.left;
20723
- let top = ar.bottom - hr.top + 6;
20724
- if (left + w > hr.width - 4) left = hr.width - w - 4;
20725
- if (left < 4) left = 4;
20726
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
20727
- if (top < 4) top = 4;
20728
- menu2.style.left = `${Math.round(left)}px`;
20729
- menu2.style.top = `${Math.round(top)}px`;
20730
- this.menuEl = menu2;
21889
+ });
20731
21890
  this.menuOwner = anchor;
20732
21891
  }
20733
21892
  closeMenu() {
20734
- if (this.menuEl) {
20735
- this.menuEl.remove();
20736
- this.menuEl = null;
20737
- }
20738
- this.menuOwner = null;
21893
+ this.menuPop?.hide();
20739
21894
  }
20740
21895
  /** A color control: an `icon` over a thin **colored underline** showing the current
20741
21896
  * value (the common idiom), with an invisible native picker overlaid to edit it.
@@ -20774,36 +21929,18 @@ var DrawingSettingsPopup = class {
20774
21929
  }
20775
21930
  /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
20776
21931
  openColorPopover(anchor, color, onChange) {
20777
- this.closeColorPopover();
20778
- this.closeMenu();
20779
21932
  const t = this.theme;
20780
- const pop = document.createElement("div");
20781
- 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;`;
20782
- applyChromeTokens(pop, t);
20783
- pop.addEventListener("pointerdown", (e) => e.stopPropagation());
20784
- pop.appendChild(buildColorPicker(color, t, onChange));
20785
- this.host.appendChild(pop);
20786
- const ar = anchor.getBoundingClientRect();
20787
- const hr = this.host.getBoundingClientRect();
20788
- const w = pop.offsetWidth;
20789
- const h = pop.offsetHeight;
20790
- let left = ar.left - hr.left;
20791
- let top = ar.bottom - hr.top + 6;
20792
- if (left + w > hr.width - 4) left = hr.width - w - 4;
20793
- if (left < 4) left = 4;
20794
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
20795
- if (top < 4) top = 4;
20796
- pop.style.left = `${Math.round(left)}px`;
20797
- pop.style.top = `${Math.round(top)}px`;
20798
- this.colorPop = pop;
21933
+ this.colorPop = this.hostFloat(anchor, {
21934
+ zIndex: 25,
21935
+ padding: "10px",
21936
+ fill: (el) => {
21937
+ el.appendChild(buildColorPicker(color, t, onChange));
21938
+ }
21939
+ });
20799
21940
  this.colorOwner = anchor;
20800
21941
  }
20801
21942
  closeColorPopover() {
20802
- if (this.colorPop) {
20803
- this.colorPop.remove();
20804
- this.colorPop = null;
20805
- }
20806
- this.colorOwner = null;
21943
+ this.colorPop?.hide();
20807
21944
  }
20808
21945
  divider() {
20809
21946
  const d = document.createElement("div");
@@ -20942,7 +22079,7 @@ var DrawingToolbar = class {
20942
22079
  this.onCollapse = options.onCollapse ?? (() => {
20943
22080
  });
20944
22081
  this.onDrawingsSync = options.onDrawingsSync ?? null;
20945
- ensureStyles5();
22082
+ ensureStyles3();
20946
22083
  this.root = document.createElement("div");
20947
22084
  this.root.className = "vela-dtb";
20948
22085
  this.styleRoot();
@@ -21425,13 +22562,13 @@ var DrawingToolbar = class {
21425
22562
  }
21426
22563
  };
21427
22564
  var MAGNET_ID = "__magnet__";
21428
- var STYLE_ID5 = "vela-drawing-toolbar-styles";
21429
- function ensureStyles5() {
22565
+ var STYLE_ID4 = "vela-drawing-toolbar-styles";
22566
+ function ensureStyles3() {
21430
22567
  if (typeof document === "undefined") return;
21431
- let s = document.getElementById(STYLE_ID5);
22568
+ let s = document.getElementById(STYLE_ID4);
21432
22569
  if (!s) {
21433
22570
  s = document.createElement("style");
21434
- s.id = STYLE_ID5;
22571
+ s.id = STYLE_ID4;
21435
22572
  document.head.appendChild(s);
21436
22573
  }
21437
22574
  s.textContent = `
@@ -21810,9 +22947,9 @@ var UserDrawingController = class {
21810
22947
  }
21811
22948
  /**
21812
22949
  * Union of the visible drawings' price ranges on `paneId` whose time extent
21813
- * intersects [fromTime, toTime] folded into the pane's autoscale so an
21814
- * off-series drawing still expands the scale (mirrors Pine drawings). Hidden
21815
- * drawings and full-width references (hline null extent) are handled too.
22950
+ * intersects [fromTime, toTime]. Hidden drawings and full-width references
22951
+ * (hline null extent) are handled too. Kept as the seam for a future
22952
+ * per-drawing autoscale opt-in the renderer does not fold this in today.
21816
22953
  */
21817
22954
  priceRangeForPane(paneId, fromTime, toTime) {
21818
22955
  let lo = Infinity;
@@ -22168,6 +23305,16 @@ var UserDrawingController = class {
22168
23305
  if (this.eraserMode) return "pointer";
22169
23306
  return this.interaction.cursorAt(x, y);
22170
23307
  }
23308
+ /** Right-click while placing: cancel the in-progress drawing and revert to the
23309
+ * pointer — the gesture is an explicit escape, so it disarms even in
23310
+ * stay-in-drawing-mode (where Escape would leave the tool armed). Returns whether
23311
+ * the press was consumed; false lets the host's context menu open normally. */
23312
+ cancelPlacement() {
23313
+ if (!this.interaction.isPlacing()) return false;
23314
+ this.interaction.cancel();
23315
+ if (this.activeTool != null) this.emit({ kind: "arm", type: null });
23316
+ return true;
23317
+ }
22171
23318
  /** Double-click over a drawing → suppress the chart's view reset (single-click already
22172
23319
  * opens settings). Returns true only when a drawing is under the cursor. */
22173
23320
  dblClick(x, y) {
@@ -22652,6 +23799,20 @@ var VolumeRenderer = class {
22652
23799
  };
22653
23800
 
22654
23801
  // src/renderers/native/layers.ts
23802
+ function foldBaseModulation(acc, next) {
23803
+ if (next == null) return acc;
23804
+ if (acc == null) return { ...next };
23805
+ return {
23806
+ candleBodyScale: minOpt(acc.candleBodyScale, next.candleBodyScale),
23807
+ candleBodyAlpha: minOpt(acc.candleBodyAlpha, next.candleBodyAlpha),
23808
+ gridAlpha: minOpt(acc.gridAlpha, next.gridAlpha)
23809
+ };
23810
+ }
23811
+ function minOpt(a, b) {
23812
+ if (a == null) return b;
23813
+ if (b == null) return a;
23814
+ return Math.min(a, b);
23815
+ }
22655
23816
  var registry3 = /* @__PURE__ */ new Map();
22656
23817
  function rendererLayers() {
22657
23818
  return [...registry3.values()];
@@ -22663,7 +23824,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
22663
23824
 
22664
23825
  // src/renderers/native/chrome/AttributionMark.ts
22665
23826
  var ATTRIBUTION_URL = "https://luxalgo.com/vela";
22666
- var STYLE_ID6 = "vela-attribution-styles";
23827
+ var STYLE_ID5 = "vela-attribution-styles";
22667
23828
  var CSS2 = `
22668
23829
  .vela-attribution {
22669
23830
  text-decoration: none;
@@ -22726,11 +23887,11 @@ var CSS2 = `
22726
23887
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
22727
23888
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
22728
23889
  `;
22729
- function ensureStyles6(doc) {
22730
- let s = doc.getElementById(STYLE_ID6);
23890
+ function ensureStyles4(doc) {
23891
+ let s = doc.getElementById(STYLE_ID5);
22731
23892
  if (!s) {
22732
23893
  s = doc.createElement("style");
22733
- s.id = STYLE_ID6;
23894
+ s.id = STYLE_ID5;
22734
23895
  doc.head.appendChild(s);
22735
23896
  }
22736
23897
  s.textContent = CSS2;
@@ -22759,7 +23920,7 @@ function createCustomMark(doc, html, background) {
22759
23920
  return el;
22760
23921
  }
22761
23922
  function createAttributionMark(doc, background) {
22762
- ensureStyles6(doc);
23923
+ ensureStyles4(doc);
22763
23924
  const a = doc.createElement("a");
22764
23925
  a.className = "vela-attribution";
22765
23926
  a.href = ATTRIBUTION_URL;
@@ -23289,7 +24450,7 @@ var NativeRenderer = class {
23289
24450
  this.scrollTargetRO = null;
23290
24451
  // eased rightOffset target while gliding back to the latest bars
23291
24452
  // Distance (px) from the plot's bottom edge to the button — tracks the bottom-most EXPANDED
23292
- // pane like the watermark: when the lower sub-panes collapse it rides up into the open pane.
24453
+ // pane: when the lower sub-panes collapse it rides up into the open pane.
23293
24454
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM;
23294
24455
  // Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
23295
24456
  // pane carries merged (own-scale) columns; the constant only clears a single-column scale.
@@ -24256,6 +25417,7 @@ var NativeRenderer = class {
24256
25417
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
24257
25418
  drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
24258
25419
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
25420
+ drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
24259
25421
  drawingsSnapMode: () => this.snapMode,
24260
25422
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
24261
25423
  drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
@@ -24507,6 +25669,8 @@ var NativeRenderer = class {
24507
25669
  this.attributionEl = null;
24508
25670
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
24509
25671
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
25672
+ this.mountContainer?.style.removeProperty("--vela-price-pane-top");
25673
+ this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
24510
25674
  this.mountContainer = null;
24511
25675
  this.wrapper?.remove();
24512
25676
  }
@@ -25596,19 +26760,18 @@ var NativeRenderer = class {
25596
26760
  theme: this.theme
25597
26761
  });
25598
26762
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
26763
+ let folded = null;
25599
26764
  for (const l of this.extLayers) {
25600
26765
  const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
25601
26766
  l.instance.render(args);
25602
- if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
25603
- const mod = l.instance.modulateBase(args);
25604
- if (mod) {
25605
- if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
25606
- if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
25607
- if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
25608
- }
25609
- }
26767
+ folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
25610
26768
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
25611
26769
  }
26770
+ if (folded) {
26771
+ if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
26772
+ if (folded.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(folded.candleBodyAlpha) * this.candleBodyAlpha;
26773
+ if (folded.gridAlpha != null) this.backend.gridAlpha = clamp012(folded.gridAlpha);
26774
+ }
25612
26775
  }
25613
26776
  const li = this.bars.length - 1;
25614
26777
  const liveActual = li >= 0 ? this.bars[li] : void 0;
@@ -25735,7 +26898,6 @@ var NativeRenderer = class {
25735
26898
  const n = this.coords.barCount;
25736
26899
  if (n === 0) return;
25737
26900
  const vr = this.coords.visibleLogicalRange();
25738
- const vtr = this.coords.visibleTimeRange();
25739
26901
  const i0 = Math.max(0, Math.floor(vr.from));
25740
26902
  const i1 = Math.min(n - 1, Math.ceil(vr.to));
25741
26903
  if (i1 < i0) return;
@@ -25753,9 +26915,7 @@ var NativeRenderer = class {
25753
26915
  }
25754
26916
  const models = this.scene.indicatorsForPane(pane.id);
25755
26917
  const masterModels = models.filter((m) => m.ownScale !== true);
25756
- let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
25757
- const udr = this.userDrawings?.priceRangeForPane(pane.id, vtr.from, vtr.to) ?? null;
25758
- if (udr) dr = dr ? { min: Math.min(dr.min, udr.min), max: Math.max(dr.max, udr.max) } : udr;
26918
+ const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
25759
26919
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
25760
26920
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
25761
26921
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -26000,6 +27160,7 @@ var NativeRenderer = class {
26000
27160
  this.paneControls?.reposition();
26001
27161
  this.repositionScrollButton();
26002
27162
  this.positionAttribution();
27163
+ this.publishPricePaneBounds();
26003
27164
  return;
26004
27165
  }
26005
27166
  const collapsed = panes.filter((p) => p.collapsed);
@@ -26017,9 +27178,10 @@ var NativeRenderer = class {
26017
27178
  this.paneControls?.reposition();
26018
27179
  this.repositionScrollButton();
26019
27180
  this.positionAttribution();
27181
+ this.publishPricePaneBounds();
26020
27182
  }
26021
- /** Pin the scroll-to-realtime button above the bottom-most EXPANDED pane's data area (like the
26022
- * watermark): with all lower sub-panes collapsed it settles into the lowest open pane. */
27183
+ /** Pin the scroll-to-realtime button above the bottom-most EXPANDED pane's data area:
27184
+ * with all lower sub-panes collapsed it settles into the lowest open pane. */
26023
27185
  repositionScrollButton() {
26024
27186
  const dataHeight = this.coords.height;
26025
27187
  if (dataHeight <= 0) return;
@@ -26063,6 +27225,21 @@ var NativeRenderer = class {
26063
27225
  this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
26064
27226
  this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
26065
27227
  }
27228
+ /** Publish the price pane's vertical insets as `--vela-price-pane-top` /
27229
+ * `--vela-price-pane-bottom` on the mount container, so the symbol watermark
27230
+ * (and any other host overlay) can clip to the price pane instead of spanning
27231
+ * study panes and the time axis. A maximized study pane collapses the box to
27232
+ * zero height — the mark does not appear on a study. */
27233
+ publishPricePaneBounds() {
27234
+ if (!this.mountContainer) return;
27235
+ const plotH = this.coords.height + TIME_AXIS_H;
27236
+ const price = this.scene.panes.get(PRICE_PANE_ID);
27237
+ const top = price ? price.bounds.top : 0;
27238
+ const height = price ? price.bounds.height : this.coords.height;
27239
+ const bottom = Math.max(0, plotH - (top + height));
27240
+ this.mountContainer.style.setProperty("--vela-price-pane-top", `${top}px`);
27241
+ this.mountContainer.style.setProperty("--vela-price-pane-bottom", `${bottom}px`);
27242
+ }
26066
27243
  /** The built-in mark, or the host's own when one is set. */
26067
27244
  buildAttributionEl() {
26068
27245
  return this.attributionHtml !== null ? createCustomMark(document, this.attributionHtml, this.theme.background) : createAttributionMark(document, this.theme.background);
@@ -26735,55 +27912,6 @@ function resolveElement(container) {
26735
27912
  if (!element) throw new Error(`[vela] container not found for selector "${container}"`);
26736
27913
  return element;
26737
27914
  }
26738
-
26739
- // src/ui/styles.ts
26740
- function injectStyles(id, css, root = document) {
26741
- if (typeof document === "undefined") return;
26742
- const host = root instanceof Document ? root.head : root;
26743
- if (root.getElementById?.(id) ?? host.querySelector(`#${CSS.escape(id)}`)) return;
26744
- const s = document.createElement("style");
26745
- s.id = id;
26746
- s.textContent = css;
26747
- host.appendChild(s);
26748
- }
26749
-
26750
- // src/ui/tokens.ts
26751
- var STATIC_ID = "vela-ui-tokens";
26752
- var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
26753
- var STATIC_CSS = `
26754
- .vela-ui, .vela-ui-layer {
26755
- ${STATIC_DECLS}
26756
- font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
26757
- box-sizing: border-box;
26758
- }
26759
- .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
26760
- .vela-icon { display: inline-flex; align-items: center; flex: none; }
26761
- .vela-icon svg { display: block; }
26762
- `;
26763
- function applyThemeTokens(el, t) {
26764
- const tokens = themeTokens(t);
26765
- for (const key in tokens) el.style.setProperty(key, tokens[key]);
26766
- }
26767
- function applyPlotOverlayTokens(host, base, config) {
26768
- const layout = config?.layout;
26769
- const t = layout?.background && layout.textColor ? { ...base, background: layout.background, textColor: layout.textColor } : base;
26770
- applyThemeTokens(host, t);
26771
- }
26772
- function ensureUIHost(el, theme) {
26773
- injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
26774
- el.classList.add("vela-ui");
26775
- if (theme) applyThemeTokens(el, theme);
26776
- }
26777
-
26778
- // src/ui/icons.ts
26779
- function iconEl(id, doc = document) {
26780
- const span = doc.createElement("span");
26781
- span.className = "vela-icon";
26782
- span.setAttribute("aria-hidden", "true");
26783
- const svg = iconMarkup(id);
26784
- if (svg) span.innerHTML = svg;
26785
- return span;
26786
- }
26787
27915
  function runMachine(machine5, props, render) {
26788
27916
  const m = new vanilla.VanillaMachine(machine5, props);
26789
27917
  const unsub = m.subscribe(render);
@@ -27025,7 +28153,10 @@ function menuController(opts) {
27025
28153
  props: {
27026
28154
  id: opts.id ?? nextUid("vela-menu"),
27027
28155
  ids: opts.triggerId ? { trigger: opts.triggerId } : void 0,
27028
- positioning: { placement: opts.placement ?? "bottom-start" },
28156
+ positioning: {
28157
+ placement: opts.placement ?? "bottom-start",
28158
+ ...opts.getAnchorRect ? { getAnchorRect: () => opts.getAnchorRect() } : {}
28159
+ },
27029
28160
  onSelect: (d) => opts.onSelect?.(d.value),
27030
28161
  onOpenChange: (d) => opts.onOpenChange?.(d.open)
27031
28162
  },
@@ -27107,6 +28238,22 @@ var MENU_CSS = `
27107
28238
  }
27108
28239
  .vela-menu-switch.on { background: var(--vela-accent-bright, var(--vela-accent)); border-color: var(--vela-accent-bright, var(--vela-accent)); }
27109
28240
  .vela-menu-switch.on::after { transform: translateX(16px); background: var(--vela-bg); }
28241
+ /* Favorite star (rows carrying \`favorite\`): reserved space always \u2014 it fades in on row
28242
+ hover instead of shifting the layout \u2014 and a starred row keeps its filled star visible.
28243
+ Same control language as the drawing toolbar's flyout stars. */
28244
+ .vela-menu-item .vela-menu-star {
28245
+ order: 98;
28246
+ /* Pad the 16px icon slot into a bigger hit target without moving the row's layout. */
28247
+ margin: -3px -5px -3px 0;
28248
+ padding: 3px 5px;
28249
+ box-sizing: content-box;
28250
+ border-radius: var(--vela-radius-sm);
28251
+ opacity: 0;
28252
+ transition: opacity 0.1s ease, background 0.1s ease;
28253
+ }
28254
+ .vela-menu-item[data-highlighted] .vela-menu-star { opacity: 0.55; }
28255
+ .vela-menu-item .vela-menu-star:hover { opacity: 1; background: var(--vela-hover-strong); }
28256
+ .vela-menu-item .vela-menu-star.vela-fav { opacity: 0.95; color: var(--vela-highlight); }
27110
28257
  .vela-menu-sep { height: 1px; margin: 4px 6px; background: var(--vela-border); }
27111
28258
  /* Submenu trigger row: a right-aligned chevron, and it stays highlighted while its own
27112
28259
  list is open so the path you came down remains readable. */
@@ -27125,9 +28272,13 @@ var Surface = class _Surface {
27125
28272
  this.items = [];
27126
28273
  /** Branch item id → the surface it opens. */
27127
28274
  this.subs = /* @__PURE__ */ new Map();
28275
+ /** Trigger rect captured when this level opened — the list stays put if the
28276
+ * trigger then moves (favorite chips shifting the caret). */
28277
+ this.pinnedAnchor = null;
27128
28278
  this.doc = doc;
27129
28279
  this.host = opts.host;
27130
28280
  this.onSelect = opts.onSelect;
28281
+ this.onFavorite = opts.onFavorite;
27131
28282
  this.positioner = doc.createElement("div");
27132
28283
  this.positioner.className = "vela-ui-layer";
27133
28284
  this.list = doc.createElement("ul");
@@ -27135,16 +28286,25 @@ var Surface = class _Surface {
27135
28286
  if (opts.minWidth) this.list.style.minWidth = opts.minWidth;
27136
28287
  this.positioner.appendChild(this.list);
27137
28288
  this.host.appendChild(this.positioner);
28289
+ const trigger = opts.trigger;
27138
28290
  this.ctrl = menuController({
27139
28291
  items: [],
27140
28292
  id: opts.id,
27141
28293
  placement: opts.placement,
27142
28294
  onSelect: (id) => this.onSelect(id),
27143
- onOpenChange: opts.onOpenChange,
28295
+ getAnchorRect: () => this.pinnedAnchor,
28296
+ onOpenChange: (open2) => {
28297
+ if (open2 && trigger) {
28298
+ const r = trigger.getBoundingClientRect();
28299
+ this.pinnedAnchor = { x: r.x, y: r.y, width: r.width, height: r.height };
28300
+ } else {
28301
+ this.pinnedAnchor = null;
28302
+ }
28303
+ opts.onOpenChange?.(open2);
28304
+ },
27144
28305
  triggerId: opts.triggerId
27145
28306
  });
27146
28307
  this.mid = String(this.ctrl.props.id);
27147
- const trigger = opts.trigger;
27148
28308
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
27149
28309
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
27150
28310
  const api = this.ctrl.connect(service);
@@ -27224,6 +28384,25 @@ var Surface = class _Surface {
27224
28384
  hint.textContent = item.hint;
27225
28385
  li.appendChild(hint);
27226
28386
  }
28387
+ if (item.favorite !== void 0 && this.onFavorite) {
28388
+ const on = item.favorite;
28389
+ const star = iconEl(on ? "star-filled" : "star", doc);
28390
+ star.classList.add("vela-menu-star");
28391
+ if (on) star.classList.add("vela-fav");
28392
+ star.removeAttribute("aria-hidden");
28393
+ star.setAttribute("role", "button");
28394
+ star.setAttribute("aria-label", on ? "Remove from favorites" : "Add to favorites");
28395
+ star.setAttribute("aria-pressed", on ? "true" : "false");
28396
+ for (const ev of ["pointerdown", "pointerup", "mousedown", "mouseup"]) {
28397
+ star.addEventListener(ev, (e) => e.stopPropagation());
28398
+ }
28399
+ star.addEventListener("click", (e) => {
28400
+ e.stopPropagation();
28401
+ e.preventDefault();
28402
+ this.onFavorite?.(item.id, !on);
28403
+ });
28404
+ li.appendChild(star);
28405
+ }
27227
28406
  if (item.toggle) {
27228
28407
  const sw = doc.createElement("span");
27229
28408
  sw.className = "vela-menu-switch" + (item.checked ? " on" : "");
@@ -27239,6 +28418,7 @@ var Surface = class _Surface {
27239
28418
  host: this.host,
27240
28419
  placement: "right-start",
27241
28420
  onSelect: this.onSelect,
28421
+ onFavorite: this.onFavorite,
27242
28422
  id: `${this.mid}--${item.id}`
27243
28423
  });
27244
28424
  sub.setItems(item.submenu ?? []);
@@ -27262,7 +28442,8 @@ var Menu = class {
27262
28442
  trigger: opts.trigger,
27263
28443
  triggerId: opts.triggerId,
27264
28444
  id: opts.id,
27265
- minWidth: opts.minWidth
28445
+ minWidth: opts.minWidth,
28446
+ onFavorite: opts.onFavorite
27266
28447
  });
27267
28448
  this.root.setItems(opts.items);
27268
28449
  }
@@ -27766,7 +28947,7 @@ var Drawer = class {
27766
28947
  };
27767
28948
 
27768
28949
  // src/widget/layout-picker.ts
27769
- var STYLE_ID7 = "vela-widget-layout-picker-v14";
28950
+ var STYLE_ID6 = "vela-widget-layout-picker-v14";
27770
28951
  var CSS3 = `
27771
28952
  .vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
27772
28953
  .vela-lp {
@@ -27871,7 +29052,7 @@ var CSS3 = `
27871
29052
  `;
27872
29053
  var GRID = 4;
27873
29054
  function layoutGridCanvas(doc) {
27874
- injectStyles(STYLE_ID7, CSS3, doc);
29055
+ injectStyles(STYLE_ID6, CSS3, doc);
27875
29056
  const el = doc.createElement("div");
27876
29057
  el.className = "vela-lp-grid";
27877
29058
  const squares = [];
@@ -27911,7 +29092,7 @@ var LayoutPicker = class {
27911
29092
  };
27912
29093
  this.opts = opts;
27913
29094
  this.doc = opts.host.ownerDocument;
27914
- injectStyles(STYLE_ID7, CSS3, this.doc);
29095
+ injectStyles(STYLE_ID6, CSS3, this.doc);
27915
29096
  const doc = this.doc;
27916
29097
  this.layer = doc.createElement("div");
27917
29098
  this.layer.className = "vela-ui-layer vela-lp-layer";
@@ -28224,6 +29405,24 @@ function timeframeMs(value) {
28224
29405
  if (!Number.isFinite(count) || count < 1) return NaN;
28225
29406
  return count * UNIT_MS[key];
28226
29407
  }
29408
+ function favoriteTimeframeChips(favorites) {
29409
+ const seen = /* @__PURE__ */ new Set();
29410
+ const out = [];
29411
+ for (const tf of favorites) {
29412
+ if (seen.has(tf)) continue;
29413
+ seen.add(tf);
29414
+ out.push(tf);
29415
+ }
29416
+ return out.sort((a, b) => {
29417
+ const ma = timeframeMs(a);
29418
+ const mb = timeframeMs(b);
29419
+ const aOk = Number.isFinite(ma);
29420
+ const bOk = Number.isFinite(mb);
29421
+ if (aOk && bOk && ma !== mb) return ma - mb;
29422
+ if (aOk !== bOk) return aOk ? -1 : 1;
29423
+ return 0;
29424
+ });
29425
+ }
28227
29426
  function timeframeLabel(value) {
28228
29427
  const parsed = parseTimeframe(value);
28229
29428
  if (!parsed.valid) return value;
@@ -28238,7 +29437,7 @@ function timeframeLabel(value) {
28238
29437
  }
28239
29438
 
28240
29439
  // src/widget/topbar.ts
28241
- var STYLE_ID8 = "vela-topbar";
29440
+ var STYLE_ID7 = "vela-topbar";
28242
29441
  var CSS4 = `
28243
29442
  .vela-widget-topbar {
28244
29443
  display: flex;
@@ -28276,6 +29475,36 @@ var CSS4 = `
28276
29475
  color: var(--vela-fg-bright);
28277
29476
  }
28278
29477
  .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); }
29478
+ /* Timeframe cluster: duration-sorted favorite chips, highlight in place, caret
29479
+ opening the full dropdown. With no favorites the caret is the merged trigger
29480
+ (label + chevron). An unstarred current value sits as an extra chip by the caret. */
29481
+ .vela-widget-tf-group { display: inline-flex; align-items: center; gap: 2px; }
29482
+ .vela-widget-tf-chips { display: inline-flex; align-items: center; gap: 2px; }
29483
+ .vela-widget-tf-chips:empty { display: none; }
29484
+ .vela-widget-tf[data-current='1'] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
29485
+ .vela-widget-tf-caret {
29486
+ all: unset;
29487
+ display: inline-flex;
29488
+ align-items: center;
29489
+ justify-content: center;
29490
+ width: 18px;
29491
+ height: 30px;
29492
+ border-radius: 4px;
29493
+ cursor: pointer;
29494
+ color: var(--vela-fg-muted);
29495
+ }
29496
+ .vela-widget-tf-caret:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
29497
+ .vela-widget-tf-caret[data-solo='1'] {
29498
+ width: auto;
29499
+ padding: 0 6px 0 9px;
29500
+ gap: 4px;
29501
+ color: var(--vela-fg-bright);
29502
+ background: var(--vela-hover-strong);
29503
+ font-size: 13px;
29504
+ font-weight: 550;
29505
+ white-space: nowrap;
29506
+ }
29507
+ .vela-widget-topbar .vela-widget-tf-caret .vela-icon { font-size: 14px; width: 14px; height: 14px; }
28279
29508
  .vela-widget-topbar .vela-icon { color: inherit; font-size: 16px; width: 16px; height: 16px; }
28280
29509
  /* Width is set in syncHairlines() to exactly one device pixel \u2014 a CSS 1px at
28281
29510
  fractional DPR (1.25, 1.5\u2026) straddles two physical pixels and siblings end
@@ -28373,16 +29602,23 @@ var Topbar = class {
28373
29602
  this.timeframe = opts.timeframe;
28374
29603
  this.priceStyle = opts.priceStyle;
28375
29604
  const doc = host.ownerDocument;
28376
- injectStyles(STYLE_ID8, CSS4, doc);
29605
+ injectStyles(STYLE_ID7, CSS4, doc);
28377
29606
  this.el = doc.createElement("div");
28378
29607
  this.el.className = "vela-widget-topbar";
28379
29608
  this.symbolEl = doc.createElement("button");
28380
29609
  this.symbolEl.className = "vela-widget-symbol";
28381
29610
  this.symbolEl.textContent = parseSymbol(opts.symbol).ticker;
28382
29611
  if (opts.onSymbolClick) this.symbolEl.addEventListener("click", opts.onSymbolClick);
28383
- this.tfButton = doc.createElement("button");
28384
- this.tfButton.className = "vela-widget-tf";
28385
- this.tfButton.textContent = timeframeLabel(this.timeframe);
29612
+ this.tfFavs = [...opts.timeframeFavorites ?? []];
29613
+ this.tfChipsHost = doc.createElement("span");
29614
+ this.tfChipsHost.className = "vela-widget-tf-chips";
29615
+ this.tfCaret = doc.createElement("button");
29616
+ this.tfCaret.className = "vela-widget-tf-caret";
29617
+ this.tfCaret.appendChild(iconEl("chevron-down", doc));
29618
+ this.tfCaret.setAttribute("aria-label", "Timeframes");
29619
+ const tfGroup = doc.createElement("span");
29620
+ tfGroup.className = "vela-widget-tf-group";
29621
+ tfGroup.append(this.tfChipsHost, this.tfCaret);
28386
29622
  this.styleButton = doc.createElement("button");
28387
29623
  this.styleButton.className = "vela-widget-style";
28388
29624
  this.renderStyleButton(doc);
@@ -28422,19 +29658,20 @@ var Topbar = class {
28422
29658
  d.className = "vela-sep";
28423
29659
  return d;
28424
29660
  };
28425
- const leading = [this.symbolEl, sep(), this.tfButton, sep(), this.styleButton, sep()];
29661
+ const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
28426
29662
  if (this.layoutButton) leading.push(this.layoutButton, sep());
28427
29663
  if (indicatorsBtn) leading.push(indicatorsBtn, sep());
28428
29664
  this.leftActionsSep = sep();
28429
29665
  this.leftActionsSep.hidden = true;
28430
29666
  this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
28431
29667
  host.appendChild(this.el);
29668
+ this.renderTfChips();
28432
29669
  this.onHairlineSync();
28433
29670
  this.hairlineRo = new ResizeObserver(this.onHairlineSync);
28434
29671
  this.hairlineRo.observe(this.el);
28435
29672
  doc.defaultView?.addEventListener("resize", this.onHairlineSync);
28436
29673
  this.renderActions();
28437
- this.tooltips.push(new Tooltip(this.tfButton, { content: "Timeframe", triggerId: "vela-topbar-tf", host }));
29674
+ this.tooltips.push(new Tooltip(this.tfCaret, { content: "Timeframe", triggerId: "vela-topbar-tf", host }));
28438
29675
  this.tooltips.push(new Tooltip(this.styleButton, { content: "Chart style", triggerId: "vela-topbar-style", host }));
28439
29676
  if (this.layoutButton && opts.layout) {
28440
29677
  this.tooltips.push(new Tooltip(this.layoutButton, { content: "Layout", triggerId: "vela-topbar-layout", host }));
@@ -28451,11 +29688,12 @@ var Topbar = class {
28451
29688
  });
28452
29689
  }
28453
29690
  this.tfMenu = new Menu({
28454
- trigger: this.tfButton,
29691
+ trigger: this.tfCaret,
28455
29692
  triggerId: "vela-topbar-tf",
28456
29693
  host,
28457
29694
  items: this.tfItems(),
28458
29695
  onSelect: (id) => opts.onTimeframe(id),
29696
+ onFavorite: (id, on) => opts.onTimeframeFavorite?.(id, on),
28459
29697
  // Timeframe labels are two-or-three characters ("1m", "4h", "1D") — the
28460
29698
  // stylesheet's default min-width would leave the list mostly empty.
28461
29699
  minWidth: "84px"
@@ -28473,9 +29711,49 @@ var Topbar = class {
28473
29711
  }
28474
29712
  setTimeframe(tf) {
28475
29713
  this.timeframe = tf;
28476
- this.tfButton.textContent = timeframeLabel(tf);
29714
+ this.tfMenu.setItems(this.tfItems());
29715
+ this.renderTfChips();
29716
+ }
29717
+ /** Reflect the favorite-timeframe set — the quick-switch chips and the dropdown stars. */
29718
+ setTimeframeFavorites(favs) {
29719
+ this.tfFavs = [...favs];
29720
+ this.renderTfChips();
28477
29721
  this.tfMenu.setItems(this.tfItems());
28478
29722
  }
29723
+ /** Rebuild the quick-switch chips (current value changed, or the favorite set did). */
29724
+ renderTfChips() {
29725
+ const doc = this.el.ownerDocument;
29726
+ this.tfChipsHost.replaceChildren();
29727
+ const stars = this.opts.onTimeframeFavorite !== void 0;
29728
+ const chips = stars ? favoriteTimeframeChips(this.tfFavs) : [];
29729
+ const currentIsFav = chips.includes(this.timeframe);
29730
+ const shown = currentIsFav || chips.length === 0 ? chips : [...chips, this.timeframe];
29731
+ for (const tf of shown) {
29732
+ const b = doc.createElement("button");
29733
+ b.className = "vela-widget-tf";
29734
+ const label = timeframeLabel(tf);
29735
+ b.textContent = label;
29736
+ if (tf === this.timeframe) {
29737
+ b.dataset.current = "1";
29738
+ b.setAttribute("aria-current", "true");
29739
+ } else {
29740
+ b.setAttribute("aria-label", `Switch timeframe to ${label}`);
29741
+ b.addEventListener("click", () => this.opts.onTimeframe(tf));
29742
+ }
29743
+ this.tfChipsHost.appendChild(b);
29744
+ }
29745
+ this.tfCaret.replaceChildren();
29746
+ if (chips.length === 0) {
29747
+ this.tfCaret.dataset.solo = "1";
29748
+ this.tfCaret.append(doc.createTextNode(timeframeLabel(this.timeframe)), iconEl("chevron-down", doc));
29749
+ this.tfCaret.setAttribute("aria-label", `Timeframe \u2014 ${timeframeLabel(this.timeframe)}`);
29750
+ } else {
29751
+ delete this.tfCaret.dataset.solo;
29752
+ this.tfCaret.appendChild(iconEl("chevron-down", doc));
29753
+ this.tfCaret.setAttribute("aria-label", "Timeframes");
29754
+ }
29755
+ this.onHairlineSync();
29756
+ }
28479
29757
  setPriceStyle(style) {
28480
29758
  this.priceStyle = style;
28481
29759
  this.renderStyleButton(this.styleButton.ownerDocument);
@@ -28552,9 +29830,9 @@ var Topbar = class {
28552
29830
  }
28553
29831
  /** Reflect a docked side panel's open state on its button — the panels toggle each other,
28554
29832
  * so the dock pushes the state rather than the button assuming it. */
28555
- setPanelActive(id, open) {
29833
+ setPanelActive(id, open2) {
28556
29834
  const btn = this.panelBtns.get(id);
28557
- if (btn) btn.dataset.active = open ? "1" : "";
29835
+ if (btn) btn.dataset.active = open2 ? "1" : "";
28558
29836
  }
28559
29837
  destroy() {
28560
29838
  this.hairlineRo?.disconnect();
@@ -28592,7 +29870,13 @@ var Topbar = class {
28592
29870
  }
28593
29871
  }
28594
29872
  tfItems() {
28595
- return this.opts.timeframes.map((tf) => ({ id: tf, label: timeframeLabel(tf), checked: tf === this.timeframe }));
29873
+ const stars = this.opts.onTimeframeFavorite !== void 0;
29874
+ return this.opts.timeframes.map((tf) => ({
29875
+ id: tf,
29876
+ label: timeframeLabel(tf),
29877
+ checked: tf === this.timeframe,
29878
+ ...stars ? { favorite: this.tfFavs.includes(tf) } : {}
29879
+ }));
28596
29880
  }
28597
29881
  styleItems() {
28598
29882
  return priceStyleIds().map((id, i) => ({
@@ -28615,10 +29899,10 @@ function fmtPrice(n, dp) {
28615
29899
  const d = dp ?? decimalsFor(n);
28616
29900
  return n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
28617
29901
  }
28618
- function fmtChange(open, close) {
28619
- if (open == null || close == null || !Number.isFinite(open) || !Number.isFinite(close) || open === 0) return "";
28620
- const diff = close - open;
28621
- const pct = diff / open * 100;
29902
+ function fmtChange(open2, close) {
29903
+ if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
29904
+ const diff = close - open2;
29905
+ const pct = diff / open2 * 100;
28622
29906
  const sign = diff >= 0 ? "+" : "";
28623
29907
  return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
28624
29908
  }
@@ -28667,7 +29951,7 @@ function tickerIconEl(doc, base, name, className) {
28667
29951
  }
28668
29952
 
28669
29953
  // src/widget/statusline.ts
28670
- var STYLE_ID9 = "vela-widget-statusline";
29954
+ var STYLE_ID8 = "vela-widget-statusline";
28671
29955
  var CSS5 = `
28672
29956
  .vela-statusline {
28673
29957
  position: absolute;
@@ -28859,7 +30143,7 @@ var Statusline = class {
28859
30143
  this.fitMode = false;
28860
30144
  this.fitRO = null;
28861
30145
  const doc = host.ownerDocument;
28862
- injectStyles(STYLE_ID9, CSS5, doc);
30146
+ injectStyles(STYLE_ID8, CSS5, doc);
28863
30147
  host.classList.add("vela-has-statusline");
28864
30148
  this.el = doc.createElement("div");
28865
30149
  this.el.className = "vela-statusline";
@@ -29034,21 +30318,21 @@ var Statusline = class {
29034
30318
  var MAX_FONT_PX = 36;
29035
30319
  var MIN_FONT_PX = 12;
29036
30320
  var FILL = 0.9;
29037
- var STYLE_ID10 = "vela-widget-watermark";
30321
+ var STYLE_ID9 = "vela-widget-watermark";
29038
30322
  var CSS6 = `
29039
30323
  .vela-watermark {
29040
30324
  position: absolute;
29041
- /* Insets follow the renderer-published gutters (mount container), so the mark
29042
- * centers and fits within the PLOT \u2014 never bleeding into the drawings toolbar
29043
- * on the left or the price scale on the right (visible in small multi-chart
29044
- * cells, where the scale is a large share of the width). */
29045
- top: 0;
29046
- bottom: 0;
30325
+ /* Insets follow the renderer-published gutters AND the price-pane vertical
30326
+ * bounds (mount container), so the mark centers on the PRICE PANE \u2014 never
30327
+ * the study panes below, the drawings toolbar, or the price scale. */
30328
+ top: var(--vela-price-pane-top, 0px);
30329
+ bottom: var(--vela-price-pane-bottom, 0px);
29047
30330
  left: var(--vela-toolbar-gutter, 0px);
29048
30331
  right: var(--vela-scale-gutter, 0px);
29049
30332
  display: flex;
29050
30333
  align-items: center;
29051
30334
  justify-content: center;
30335
+ overflow: hidden;
29052
30336
  pointer-events: none;
29053
30337
  z-index: 1;
29054
30338
  color: var(--vela-fg);
@@ -29073,7 +30357,7 @@ var Watermark = class {
29073
30357
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
29074
30358
  * fires during chart construction, before any subscriber can see it. */
29075
30359
  this.loading = true;
29076
- injectStyles(STYLE_ID10, CSS6, host.ownerDocument);
30360
+ injectStyles(STYLE_ID9, CSS6, host.ownerDocument);
29077
30361
  this.el = host.ownerDocument.createElement("div");
29078
30362
  this.el.className = "vela-watermark";
29079
30363
  this.el.dataset.velaScreenshot = "under";
@@ -29103,7 +30387,7 @@ var Watermark = class {
29103
30387
  this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
29104
30388
  this.fit();
29105
30389
  }
29106
- /** Measure the text at the cap and shrink it to the chart's own width. */
30390
+ /** Measure the text at the cap and shrink it to the price pane's width. */
29107
30391
  fit() {
29108
30392
  if (this.el.clientWidth <= 0 || !this.text.textContent) return;
29109
30393
  this.el.style.fontSize = `${MAX_FONT_PX}px`;
@@ -29137,7 +30421,7 @@ var RANGE_PRESETS = [
29137
30421
  { id: "ALL", tf: "W", preset: "ALL", bars: 5e3 }
29138
30422
  // everything the provider serves
29139
30423
  ];
29140
- var STYLE_ID11 = "vela-widget-bottombar";
30424
+ var STYLE_ID10 = "vela-widget-bottombar";
29141
30425
  var CSS7 = `
29142
30426
  .vela-widget-bottombar {
29143
30427
  display: flex;
@@ -29213,7 +30497,7 @@ var Bottombar = class {
29213
30497
  this.timer = null;
29214
30498
  this.timezone = opts.timezone;
29215
30499
  const doc = host.ownerDocument;
29216
- injectStyles(STYLE_ID11, CSS7, doc);
30500
+ injectStyles(STYLE_ID10, CSS7, doc);
29217
30501
  this.el = doc.createElement("div");
29218
30502
  this.el.className = "vela-widget-bottombar";
29219
30503
  for (const preset of RANGE_PRESETS) {
@@ -29351,7 +30635,7 @@ function filterSymbols(list, query, limit = 100) {
29351
30635
  }
29352
30636
  return [...prefix, ...substr, ...desc, ...venue].slice(0, limit);
29353
30637
  }
29354
- var STYLE_ID12 = "vela-widget-symbolpicker";
30638
+ var STYLE_ID11 = "vela-widget-symbolpicker";
29355
30639
  var CSS8 = `
29356
30640
  .vela-sp-searchrow {
29357
30641
  display: flex;
@@ -29449,7 +30733,7 @@ var SymbolPicker = class {
29449
30733
  this.seed = "";
29450
30734
  this.activeTab = "All";
29451
30735
  const doc = (opts.host ?? document.body).ownerDocument;
29452
- injectStyles(STYLE_ID12, CSS8, doc);
30736
+ injectStyles(STYLE_ID11, CSS8, doc);
29453
30737
  this.input = doc.createElement("input");
29454
30738
  this.input.className = "vela-sp-input";
29455
30739
  this.input.placeholder = "Search symbol\u2026";
@@ -29479,8 +30763,8 @@ var SymbolPicker = class {
29479
30763
  host: opts.host,
29480
30764
  closeOnInteractOutside: true,
29481
30765
  content: (body) => body.append(searchRow, this.tabs, this.list),
29482
- onOpenChange: (open) => {
29483
- if (open) {
30766
+ onOpenChange: (open2) => {
30767
+ if (open2) {
29484
30768
  this.input.value = this.seed;
29485
30769
  this.seed = "";
29486
30770
  this.refresh();
@@ -29489,7 +30773,7 @@ var SymbolPicker = class {
29489
30773
  this.input.setSelectionRange(this.input.value.length, this.input.value.length);
29490
30774
  }, 0);
29491
30775
  }
29492
- opts.onOpenChange?.(open);
30776
+ opts.onOpenChange?.(open2);
29493
30777
  }
29494
30778
  });
29495
30779
  this.input.addEventListener("input", () => this.refresh());
@@ -29771,7 +31055,7 @@ function treeIsEmpty(panes) {
29771
31055
  }
29772
31056
 
29773
31057
  // src/widget/side-panel.ts
29774
- var STYLE_ID13 = "vela-widget-sidepanel";
31058
+ var STYLE_ID12 = "vela-widget-sidepanel";
29775
31059
  var DEFAULT_PANEL_WIDTH = 280;
29776
31060
  var DEFAULT_PANEL_MIN_WIDTH = 200;
29777
31061
  var DEFAULT_PANEL_MAX_WIDTH = 640;
@@ -29875,7 +31159,7 @@ var SidePanel = class {
29875
31159
  * a programmatic {@link setWidth}, so restoring a persisted width raises no change. */
29876
31160
  this.onWidthChange = null;
29877
31161
  const doc = host.ownerDocument;
29878
- injectStyles(STYLE_ID13, CSS9, doc);
31162
+ injectStyles(STYLE_ID12, CSS9, doc);
29879
31163
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
29880
31164
  this.maxWidth = opts.resizable ? opts.maxWidth ?? DEFAULT_PANEL_MAX_WIDTH : Number.POSITIVE_INFINITY;
29881
31165
  this.declaredWidth = clampPanelWidth(opts.width ?? DEFAULT_PANEL_WIDTH, this.minWidth, this.maxWidth);
@@ -29907,10 +31191,10 @@ var SidePanel = class {
29907
31191
  return !this.el.hidden;
29908
31192
  }
29909
31193
  /** Open/close the panel — a bare call flips it. */
29910
- toggle(open = this.el.hidden) {
29911
- if (open === !this.el.hidden) return;
29912
- this.el.hidden = !open;
29913
- this.onOpenChange?.(open);
31194
+ toggle(open2 = this.el.hidden) {
31195
+ if (open2 === !this.el.hidden) return;
31196
+ this.el.hidden = !open2;
31197
+ this.onOpenChange?.(open2);
29914
31198
  }
29915
31199
  /** The scrolling body, for a panel filled from OUTSIDE the class — a contributed panel's
29916
31200
  * `mount` receives exactly this element. Subclasses use the protected `body`. */
@@ -29984,7 +31268,7 @@ var SidePanel = class {
29984
31268
  };
29985
31269
 
29986
31270
  // src/widget/object-tree.ts
29987
- var STYLE_ID14 = "vela-widget-objtree";
31271
+ var STYLE_ID13 = "vela-widget-objtree";
29988
31272
  var CSS10 = `
29989
31273
  /* One pane's block. The transparent border reserves the drop-target outline. */
29990
31274
  .vela-ot-pane {
@@ -30223,7 +31507,7 @@ var ObjectTree = class extends SidePanel {
30223
31507
  this.menu = null;
30224
31508
  this.menuActions = /* @__PURE__ */ new Map();
30225
31509
  this.drag = null;
30226
- injectStyles(STYLE_ID14, CSS10, host.ownerDocument);
31510
+ injectStyles(STYLE_ID13, CSS10, host.ownerDocument);
30227
31511
  this.body.addEventListener("contextmenu", (e) => this.onContextMenu(e));
30228
31512
  this.body.addEventListener("pointerdown", (e) => this.onPointerDown(e));
30229
31513
  }
@@ -30233,9 +31517,9 @@ var ObjectTree = class extends SidePanel {
30233
31517
  set groups(next) {
30234
31518
  if (this.chart) this.groupsPerChart.set(this.chart, next);
30235
31519
  }
30236
- toggle(open = this.el.hidden) {
30237
- super.toggle(open);
30238
- if (open) this.refresh();
31520
+ toggle(open2 = this.el.hidden) {
31521
+ super.toggle(open2);
31522
+ if (open2) this.refresh();
30239
31523
  }
30240
31524
  setSymbol(symbol) {
30241
31525
  this.symbolName = parseSymbol(symbol).ticker;
@@ -31205,7 +32489,7 @@ var ObjectTree = class extends SidePanel {
31205
32489
  };
31206
32490
 
31207
32491
  // src/widget/data-window.ts
31208
- var STYLE_ID15 = "vela-widget-datawindow";
32492
+ var STYLE_ID14 = "vela-widget-datawindow";
31209
32493
  var CSS11 = `
31210
32494
  .vela-dw-group {
31211
32495
  padding: 10px 8px 4px;
@@ -31265,11 +32549,11 @@ var DataWindow = class extends SidePanel {
31265
32549
  super(host, "Data window", "vela-dw");
31266
32550
  this.chart = null;
31267
32551
  this.unsubs = [];
31268
- injectStyles(STYLE_ID15, CSS11, host.ownerDocument);
32552
+ injectStyles(STYLE_ID14, CSS11, host.ownerDocument);
31269
32553
  }
31270
- toggle(open = this.el.hidden) {
31271
- super.toggle(open);
31272
- if (open) this.refresh();
32554
+ toggle(open2 = this.el.hidden) {
32555
+ super.toggle(open2);
32556
+ if (open2) this.refresh();
31273
32557
  }
31274
32558
  /** (Re)bind to a chart instance — called after every widget rebuild. */
31275
32559
  onChart(chart) {
@@ -31383,8 +32667,8 @@ var PanelDock = class {
31383
32667
  }
31384
32668
  }
31385
32669
  /** Open/close one panel by id — a bare call flips it. Unknown ids are ignored. */
31386
- toggle(id, open) {
31387
- this.entries.find((e) => e.id === id)?.panel.toggle(open);
32670
+ toggle(id, open2) {
32671
+ this.entries.find((e) => e.id === id)?.panel.toggle(open2);
31388
32672
  }
31389
32673
  /** The open panel's id, or null when the column is closed. */
31390
32674
  get openId() {
@@ -31398,8 +32682,8 @@ var PanelDock = class {
31398
32682
  /** The dock's persistable state, or null when there is nothing worth saving. */
31399
32683
  getState() {
31400
32684
  const out = {};
31401
- const open = this.openId;
31402
- if (open) out.open = open;
32685
+ const open2 = this.openId;
32686
+ if (open2) out.open = open2;
31403
32687
  if (this.widths.size > 0) out.widths = Object.fromEntries(this.widths);
31404
32688
  return out.open || out.widths ? out : null;
31405
32689
  }
@@ -31432,13 +32716,13 @@ var PanelDock = class {
31432
32716
  this.entries.sort((a, b) => a.order - b.order);
31433
32717
  const stored = this.widths.get(entry.id);
31434
32718
  if (stored !== void 0) entry.panel.setWidth(stored);
31435
- entry.panel.onOpenChange = (open) => {
31436
- if (open) {
32719
+ entry.panel.onOpenChange = (open2) => {
32720
+ if (open2) {
31437
32721
  for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
31438
32722
  this.pendingOpen = null;
31439
32723
  }
31440
- this.deps.chrome.setPanelActive(entry.id, open);
31441
- if (open) entry.handle?.onOpen?.();
32724
+ this.deps.chrome.setPanelActive(entry.id, open2);
32725
+ if (open2) entry.handle?.onOpen?.();
31442
32726
  this.deps.changed?.();
31443
32727
  };
31444
32728
  entry.panel.onWidthChange = (px) => {
@@ -31467,7 +32751,7 @@ var PanelDock = class {
31467
32751
  };
31468
32752
 
31469
32753
  // src/widget/shortcuts-help.ts
31470
- var STYLE_ID16 = "vela-widget-shortcuts";
32754
+ var STYLE_ID15 = "vela-widget-shortcuts";
31471
32755
  var CSS12 = `
31472
32756
  .vela-sh-cat {
31473
32757
  color: var(--vela-fg-muted);
@@ -31493,16 +32777,16 @@ var ShortcutsHelp = class {
31493
32777
  constructor(keymap, host, onOpenChange) {
31494
32778
  this.keymap = keymap;
31495
32779
  const doc = (host ?? document.body).ownerDocument;
31496
- injectStyles(STYLE_ID16, CSS12, doc);
32780
+ injectStyles(STYLE_ID15, CSS12, doc);
31497
32781
  this.list = doc.createElement("div");
31498
32782
  this.dialog = new Dialog({
31499
32783
  title: "Keyboard shortcuts",
31500
32784
  host,
31501
32785
  closeOnInteractOutside: true,
31502
32786
  content: (body) => body.appendChild(this.list),
31503
- onOpenChange: (open) => {
31504
- if (open) this.refresh();
31505
- onOpenChange?.(open);
32787
+ onOpenChange: (open2) => {
32788
+ if (open2) this.refresh();
32789
+ onOpenChange?.(open2);
31506
32790
  }
31507
32791
  });
31508
32792
  }
@@ -31781,7 +33065,7 @@ var ChartContextMenu = class {
31781
33065
  };
31782
33066
 
31783
33067
  // src/widget/indicator-picker.ts
31784
- var STYLE_ID17 = "vela-widget-indpicker";
33068
+ var STYLE_ID16 = "vela-widget-indpicker";
31785
33069
  var CSS13 = `
31786
33070
  .vela-ip-searchrow {
31787
33071
  display: flex;
@@ -31859,7 +33143,7 @@ var IndicatorPicker = class {
31859
33143
  this.isOpen = false;
31860
33144
  this.opts = opts;
31861
33145
  const doc = (opts.host ?? document.body).ownerDocument;
31862
- injectStyles(STYLE_ID17, CSS13, doc);
33146
+ injectStyles(STYLE_ID16, CSS13, doc);
31863
33147
  this.search = doc.createElement("input");
31864
33148
  this.search.className = "vela-ip-search";
31865
33149
  this.search.placeholder = "Search\u2026";
@@ -31877,14 +33161,14 @@ var IndicatorPicker = class {
31877
33161
  closeOnInteractOutside: true,
31878
33162
  // The picker adds/removes indicators live — keep the chart undimmed behind it.
31879
33163
  content: (body) => body.append(searchRow, this.list),
31880
- onOpenChange: (open) => {
31881
- this.isOpen = open;
31882
- if (open) {
33164
+ onOpenChange: (open2) => {
33165
+ this.isOpen = open2;
33166
+ if (open2) {
31883
33167
  this.search.value = "";
31884
33168
  this.refresh();
31885
33169
  if (!this.search.closest('[data-layout="mobile"]')) setTimeout(() => this.search.focus(), 0);
31886
33170
  }
31887
- opts.onOpenChange?.(open);
33171
+ opts.onOpenChange?.(open2);
31888
33172
  }
31889
33173
  });
31890
33174
  this.list.addEventListener("click", (e) => {
@@ -31987,7 +33271,7 @@ var IndicatorPicker = class {
31987
33271
  };
31988
33272
 
31989
33273
  // src/widget/timeframe-quick.ts
31990
- var STYLE_ID18 = "vela-widget-tfquick";
33274
+ var STYLE_ID17 = "vela-widget-tfquick";
31991
33275
  var CSS14 = `
31992
33276
  .vela-tq-input {
31993
33277
  display: block;
@@ -32011,7 +33295,7 @@ var CSS14 = `
32011
33295
  var TimeframeQuick = class {
32012
33296
  constructor(opts) {
32013
33297
  const doc = (opts.host ?? document.body).ownerDocument;
32014
- injectStyles(STYLE_ID18, CSS14, doc);
33298
+ injectStyles(STYLE_ID17, CSS14, doc);
32015
33299
  this.input = doc.createElement("input");
32016
33300
  this.input.className = "vela-tq-input";
32017
33301
  this.input.setAttribute("spellcheck", "false");
@@ -32023,7 +33307,7 @@ var TimeframeQuick = class {
32023
33307
  draggable: true,
32024
33308
  closeOnInteractOutside: true,
32025
33309
  content: (body) => body.append(this.input, this.hint),
32026
- onOpenChange: (open) => opts.onOpenChange?.(open)
33310
+ onOpenChange: (open2) => opts.onOpenChange?.(open2)
32027
33311
  });
32028
33312
  this.input.addEventListener("input", () => this.renderHint());
32029
33313
  this.input.addEventListener("keydown", (e) => {
@@ -32193,7 +33477,7 @@ var LayoutModeController = class {
32193
33477
  };
32194
33478
 
32195
33479
  // src/widget/mobile-bar.ts
32196
- var STYLE_ID19 = "vela-widget-mobilebar";
33480
+ var STYLE_ID18 = "vela-widget-mobilebar";
32197
33481
  var CSS15 = `
32198
33482
  .vela-mobilebar {
32199
33483
  display: none;
@@ -32244,7 +33528,7 @@ var MobileBar = class {
32244
33528
  constructor(host, opts) {
32245
33529
  this.opts = opts;
32246
33530
  const doc = host.ownerDocument;
32247
- injectStyles(STYLE_ID19, CSS15, doc);
33531
+ injectStyles(STYLE_ID18, CSS15, doc);
32248
33532
  this.el = doc.createElement("div");
32249
33533
  this.el.className = "vela-mobilebar";
32250
33534
  const item = (cls, label, onClick, icon2) => {
@@ -32303,7 +33587,7 @@ var MobileBar = class {
32303
33587
  };
32304
33588
 
32305
33589
  // src/widget/timeframe-drawer.ts
32306
- var STYLE_ID20 = "vela-widget-tf-drawer";
33590
+ var STYLE_ID19 = "vela-widget-tf-drawer";
32307
33591
  var CSS16 = `
32308
33592
  .vela-tfd-heading {
32309
33593
  padding: 4px 2px 8px;
@@ -32360,7 +33644,7 @@ var TimeframeDrawer = class {
32360
33644
  this.rangeChips = /* @__PURE__ */ new Map();
32361
33645
  this.tfChips = /* @__PURE__ */ new Map();
32362
33646
  const doc = opts.host.ownerDocument;
32363
- injectStyles(STYLE_ID20, CSS16, doc);
33647
+ injectStyles(STYLE_ID19, CSS16, doc);
32364
33648
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
32365
33649
  const rangeHeading = doc.createElement("div");
32366
33650
  rangeHeading.className = "vela-tfd-heading";
@@ -32418,7 +33702,7 @@ var TimeframeDrawer = class {
32418
33702
  };
32419
33703
 
32420
33704
  // src/widget/drawings-drawer.ts
32421
- var STYLE_ID21 = "vela-widget-drawings-drawer";
33705
+ var STYLE_ID20 = "vela-widget-drawings-drawer";
32422
33706
  var CSS17 = `
32423
33707
  /* Search + tabs stay pinned while the tool list scrolls underneath. */
32424
33708
  .vela-dd-sticky {
@@ -32521,7 +33805,7 @@ var DrawingsDrawer = class {
32521
33805
  this.definition = { groups: [] };
32522
33806
  this.activeGroup = "";
32523
33807
  const doc = opts.host.ownerDocument;
32524
- injectStyles(STYLE_ID21, CSS17, doc);
33808
+ injectStyles(STYLE_ID20, CSS17, doc);
32525
33809
  this.drawer = new Drawer({
32526
33810
  host: opts.host,
32527
33811
  title: "Drawings",
@@ -32663,7 +33947,7 @@ var DrawingsDrawer = class {
32663
33947
  };
32664
33948
 
32665
33949
  // src/widget/more-drawer.ts
32666
- var STYLE_ID22 = "vela-widget-more-drawer";
33950
+ var STYLE_ID21 = "vela-widget-more-drawer";
32667
33951
  var CSS18 = `
32668
33952
  .vela-md-actions {
32669
33953
  display: flex;
@@ -32739,7 +34023,7 @@ var MoreDrawer = class {
32739
34023
  constructor(opts) {
32740
34024
  this.opts = opts;
32741
34025
  this.view = "main";
32742
- injectStyles(STYLE_ID22, CSS18, opts.host.ownerDocument);
34026
+ injectStyles(STYLE_ID21, CSS18, opts.host.ownerDocument);
32743
34027
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
32744
34028
  }
32745
34029
  open() {
@@ -32931,7 +34215,7 @@ var MoreDrawer = class {
32931
34215
  };
32932
34216
 
32933
34217
  // src/widget/timezone-drawer.ts
32934
- var STYLE_ID23 = "vela-widget-timezone-drawer";
34218
+ var STYLE_ID22 = "vela-widget-timezone-drawer";
32935
34219
  var CSS19 = `
32936
34220
  .vela-tzd-list { padding: 2px 0 4px; }
32937
34221
  .vela-tzd-row {
@@ -32951,7 +34235,7 @@ var CSS19 = `
32951
34235
  var TimezoneDrawer = class {
32952
34236
  constructor(opts) {
32953
34237
  this.opts = opts;
32954
- injectStyles(STYLE_ID23, CSS19, opts.host.ownerDocument);
34238
+ injectStyles(STYLE_ID22, CSS19, opts.host.ownerDocument);
32955
34239
  this.drawer = new Drawer({ host: opts.host, title: "Time zone", onOpenChange: opts.onOpenChange });
32956
34240
  }
32957
34241
  open() {
@@ -32989,7 +34273,7 @@ var TimezoneDrawer = class {
32989
34273
  };
32990
34274
 
32991
34275
  // src/widget/price-scale-drawer.ts
32992
- var STYLE_ID24 = "vela-widget-price-scale-drawer";
34276
+ var STYLE_ID23 = "vela-widget-price-scale-drawer";
32993
34277
  var CSS20 = `
32994
34278
  .vela-psd-list { padding: 2px 0 4px; }
32995
34279
  .vela-psd-row {
@@ -33019,7 +34303,7 @@ var PriceScaleDrawer = class {
33019
34303
  constructor(opts) {
33020
34304
  this.opts = opts;
33021
34305
  this.pane = null;
33022
- injectStyles(STYLE_ID24, CSS20, opts.host.ownerDocument);
34306
+ injectStyles(STYLE_ID23, CSS20, opts.host.ownerDocument);
33023
34307
  this.drawer = new Drawer({ host: opts.host, title: "Price scale", onOpenChange: opts.onOpenChange });
33024
34308
  }
33025
34309
  open() {
@@ -33155,7 +34439,7 @@ var PriceScaleDrawer = class {
33155
34439
  };
33156
34440
 
33157
34441
  // src/widget/drawing-pill.ts
33158
- var STYLE_ID25 = "vela-widget-drawing-pill";
34442
+ var STYLE_ID24 = "vela-widget-drawing-pill";
33159
34443
  var CSS21 = `
33160
34444
  .vela-drawpill { display: none; }
33161
34445
  [data-layout='mobile'] .vela-drawpill {
@@ -33216,7 +34500,7 @@ var DrawingPill = class {
33216
34500
  this.chart = null;
33217
34501
  this.chartSubs = [];
33218
34502
  const doc = host.ownerDocument;
33219
- injectStyles(STYLE_ID25, CSS21, doc);
34503
+ injectStyles(STYLE_ID24, CSS21, doc);
33220
34504
  this.el = doc.createElement("div");
33221
34505
  this.el.className = "vela-drawpill";
33222
34506
  this.el.hidden = true;
@@ -33325,6 +34609,10 @@ function sanitizeState(doc) {
33325
34609
  const favs = d.favorites.filter((f) => typeof f === "string");
33326
34610
  if (favs.length > 0) out.favorites = favs;
33327
34611
  }
34612
+ if (Array.isArray(d.timeframeFavorites)) {
34613
+ const favs = d.timeframeFavorites.filter((f) => typeof f === "string");
34614
+ if (favs.length > 0) out.timeframeFavorites = favs;
34615
+ }
33328
34616
  const sync = sanitizeSync(d.sync);
33329
34617
  if (sync) out.sync = sync;
33330
34618
  const tracks = sanitizeTrackSizes(d.trackSizes);
@@ -33598,7 +34886,7 @@ var WidgetHistory = class {
33598
34886
  };
33599
34887
 
33600
34888
  // src/widget/toast.ts
33601
- var STYLE_ID26 = "vela-widget-toast";
34889
+ var STYLE_ID25 = "vela-widget-toast";
33602
34890
  var CSS22 = `
33603
34891
  .vela-toast {
33604
34892
  position: absolute;
@@ -33629,7 +34917,7 @@ var CSS22 = `
33629
34917
  var Toast = class {
33630
34918
  constructor(host) {
33631
34919
  this.timer = null;
33632
- injectStyles(STYLE_ID26, CSS22, host.ownerDocument);
34920
+ injectStyles(STYLE_ID25, CSS22, host.ownerDocument);
33633
34921
  this.el = host.ownerDocument.createElement("div");
33634
34922
  this.el.className = "vela-toast";
33635
34923
  host.appendChild(this.el);
@@ -33765,6 +35053,8 @@ var VelaWidget = class {
33765
35053
  this.stateListeners = /* @__PURE__ */ new Set();
33766
35054
  /** Favorite drawing-tool types — mirrored from chart events, reapplied on rebuilds. */
33767
35055
  this.favs = [];
35056
+ /** Favorite timeframes — the topbar's quick-switch chips, persisted with the shell. */
35057
+ this.tfFavs = [];
33768
35058
  /** Mounted attachment disposers (by id), torn down at destroy. */
33769
35059
  this.attachmentDisposers = /* @__PURE__ */ new Map();
33770
35060
  this.onUnload = () => this.persistNow();
@@ -33807,6 +35097,7 @@ var VelaWidget = class {
33807
35097
  this.indicatorTitlesOn = bootCell?.indicatorTitles ?? true;
33808
35098
  this.indicatorValuesOn = bootCell?.indicatorValues ?? true;
33809
35099
  this.favs = boot?.favorites ? [...boot.favorites] : [];
35100
+ this.tfFavs = boot?.timeframeFavorites ? [...boot.timeframeFavorites] : [];
33810
35101
  this.savedConfig = bootCell?.rendererConfig ?? null;
33811
35102
  this.savedDrawings = bootCell?.drawings ?? null;
33812
35103
  this.pendingIndicators = bootCell?.indicators ?? null;
@@ -33820,9 +35111,9 @@ var VelaWidget = class {
33820
35111
  this.symbolPicker = new SymbolPicker({
33821
35112
  host: this.root,
33822
35113
  onSelect: (symbol) => this.setSymbol(symbol),
33823
- onOpenChange: (open) => {
33824
- if (open) this.inner?.renderer.closeDialogs();
33825
- this.trackDialog(open);
35114
+ onOpenChange: (open2) => {
35115
+ if (open2) this.inner?.renderer.closeDialogs();
35116
+ this.trackDialog(open2);
33826
35117
  }
33827
35118
  });
33828
35119
  this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
@@ -33845,15 +35136,15 @@ var VelaWidget = class {
33845
35136
  if (i < present.length) this.removeNative(present[i].type);
33846
35137
  else this.removeInstance(i - present.length);
33847
35138
  },
33848
- onOpenChange: (open) => {
33849
- if (open) this.inner?.renderer.closeDialogs();
33850
- this.trackDialog(open);
35139
+ onOpenChange: (open2) => {
35140
+ if (open2) this.inner?.renderer.closeDialogs();
35141
+ this.trackDialog(open2);
33851
35142
  }
33852
35143
  }) : null;
33853
35144
  this.tfQuick = new TimeframeQuick({
33854
35145
  host: this.root,
33855
35146
  onApply: (tf) => this.setTimeframe(tf),
33856
- onOpenChange: (open) => this.trackDialog(open)
35147
+ onOpenChange: (open2) => this.trackDialog(open2)
33857
35148
  });
33858
35149
  const picker = this.indicatorPicker;
33859
35150
  this.topbar = new Topbar(this.root, {
@@ -33866,8 +35157,10 @@ var VelaWidget = class {
33866
35157
  onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
33867
35158
  timeframe: this.timeframe,
33868
35159
  timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
35160
+ timeframeFavorites: this.tfFavs,
33869
35161
  priceStyle: this.priceStyle,
33870
35162
  onTimeframe: (tf) => this.setTimeframe(tf),
35163
+ onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
33871
35164
  onPriceStyle: (style) => this.setPriceStyle(style),
33872
35165
  getContext: () => this.context()
33873
35166
  });
@@ -33978,7 +35271,7 @@ var VelaWidget = class {
33978
35271
  label: "Show this shortcuts panel",
33979
35272
  category: "Help",
33980
35273
  run: () => {
33981
- this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open) => this.trackDialog(open)));
35274
+ this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open2) => this.trackDialog(open2)));
33982
35275
  this.shortcutsHelp.open();
33983
35276
  }
33984
35277
  });
@@ -34012,7 +35305,7 @@ var VelaWidget = class {
34012
35305
  activeRange: () => this.activeRangeId,
34013
35306
  onTimeframe: (tf) => this.setTimeframe(tf),
34014
35307
  onRange: (preset) => this.applyRange(preset),
34015
- onOpenChange: (open) => this.trackDialog(open)
35308
+ onOpenChange: (open2) => this.trackDialog(open2)
34016
35309
  }));
34017
35310
  this.tfDrawer.open();
34018
35311
  }
@@ -34024,7 +35317,7 @@ var VelaWidget = class {
34024
35317
  isFavorite: (type) => this.inner?.drawings.isFavorite(type) ?? false,
34025
35318
  onFavorite: (type, on) => this.inner?.drawings.setFavorite(type, on),
34026
35319
  onSelect: (type) => this.inner?.drawings.setTool(type),
34027
- onOpenChange: (open) => this.trackDialog(open)
35320
+ onOpenChange: (open2) => this.trackDialog(open2)
34028
35321
  }));
34029
35322
  this.drawingsDrawer.open();
34030
35323
  }
@@ -34045,7 +35338,7 @@ var VelaWidget = class {
34045
35338
  // Left-aligned actions have their own bottom-bar stop — only the rest
34046
35339
  // lands in the drawer, or every left action would appear twice.
34047
35340
  actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
34048
- onOpenChange: (open) => this.trackDialog(open)
35341
+ onOpenChange: (open2) => this.trackDialog(open2)
34049
35342
  }));
34050
35343
  this.moreDrawer.open();
34051
35344
  }
@@ -34054,7 +35347,7 @@ var VelaWidget = class {
34054
35347
  host: this.root,
34055
35348
  timezone: () => this.timezone,
34056
35349
  onTimezone: (zone) => this.setTimezone(zone),
34057
- onOpenChange: (open) => this.trackDialog(open)
35350
+ onOpenChange: (open2) => this.trackDialog(open2)
34058
35351
  }));
34059
35352
  this.timezoneDrawer.open();
34060
35353
  }
@@ -34064,7 +35357,7 @@ var VelaWidget = class {
34064
35357
  host: this.root,
34065
35358
  chart: () => this.inner,
34066
35359
  pressY: () => this.priceScalePressY,
34067
- onOpenChange: (open) => this.trackDialog(open)
35360
+ onOpenChange: (open2) => this.trackDialog(open2)
34068
35361
  }));
34069
35362
  this.priceScaleDrawer.open();
34070
35363
  }
@@ -34093,7 +35386,7 @@ var VelaWidget = class {
34093
35386
  setTimeframe: (tf) => this.setTimeframe(tf),
34094
35387
  setPriceStyle: (style) => this.setPriceStyle(style),
34095
35388
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
34096
- togglePanel: (id, open) => this.dock.toggle(id, open),
35389
+ togglePanel: (id, open2) => this.dock.toggle(id, open2),
34097
35390
  host: this.root,
34098
35391
  toast: (message, kind) => this.toast?.show(message, kind)
34099
35392
  };
@@ -34152,6 +35445,14 @@ var VelaWidget = class {
34152
35445
  this.markStateDirty();
34153
35446
  void this.inner?.setMarket({ timeframe: tf, bars: this.bars });
34154
35447
  }
35448
+ /** Star/unstar a timeframe — the topbar chips and dropdown stars follow, and the
35449
+ * set persists with the rest of the shell state. */
35450
+ setTimeframeFavorite(tf, on) {
35451
+ if (on === this.tfFavs.includes(tf)) return;
35452
+ this.tfFavs = on ? [tf, ...this.tfFavs] : this.tfFavs.filter((f) => f !== tf);
35453
+ this.topbar.setTimeframeFavorites(this.tfFavs);
35454
+ this.markStateDirty();
35455
+ }
34155
35456
  /**
34156
35457
  * Switch the chart symbol in place. An `EXCHANGE:` prefix pins the venue (the
34157
35458
  * symbol picker composes one from the row the user pointed at) — a bare ticker
@@ -34209,6 +35510,7 @@ var VelaWidget = class {
34209
35510
  });
34210
35511
  const state = { version: 1, layout: "1", activeCellId: "c1", timezone: this.timezone, charts: [{ id: "c1", ...cell }] };
34211
35512
  if (this.favs.length > 0) state.favorites = [...this.favs];
35513
+ if (this.tfFavs.length > 0) state.timeframeFavorites = [...this.tfFavs];
34212
35514
  const panels2 = this.dock.getState();
34213
35515
  if (panels2) state.panels = panels2;
34214
35516
  return state;
@@ -34232,6 +35534,10 @@ var VelaWidget = class {
34232
35534
  this.favs = [...st.favorites];
34233
35535
  this.inner?.drawings.setFavorites(this.favs);
34234
35536
  }
35537
+ if (st.timeframeFavorites) {
35538
+ this.tfFavs = [...st.timeframeFavorites];
35539
+ this.topbar.setTimeframeFavorites(this.tfFavs);
35540
+ }
34235
35541
  this.dock.applyState(st.panels);
34236
35542
  if (cell) {
34237
35543
  const style = fromUrl.priceStyle ?? cell.priceStyle;
@@ -34806,9 +36112,9 @@ var VelaWidget = class {
34806
36112
  this.tfQuick.open(key);
34807
36113
  }
34808
36114
  }
34809
- trackDialog(open) {
34810
- this.openDialogs = Math.max(0, this.openDialogs + (open ? 1 : -1));
34811
- if (open) this.keymap.pushScope("dialog");
36115
+ trackDialog(open2) {
36116
+ this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));
36117
+ if (open2) this.keymap.pushScope("dialog");
34812
36118
  else this.keymap.popScope("dialog");
34813
36119
  }
34814
36120
  downloadScreenshot() {