@luxalgo/vela 0.5.3 → 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-L5NTMWPP.js → chunk-D6WM5IUE.js} +724 -851
  5. package/dist/{chunk-LJLZR44Y.js → chunk-EMS6PO2T.js} +5 -7
  6. package/dist/{chunk-EY63XBFZ.js → chunk-JBQUY2RI.js} +119 -12
  7. package/dist/chunk-OGRQW3M6.js +1328 -0
  8. package/dist/{chunk-P556H6EA.js → chunk-QWIEKZRE.js} +59 -34
  9. package/dist/{chunk-WJKLBYRM.js → chunk-ZNL2X6U2.js} +1 -1
  10. package/dist/{contributions-Ci_i3IN2.d.cts → contributions-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 +2715 -1606
  15. package/dist/index.d.cts +6 -6
  16. package/dist/index.d.ts +6 -6
  17. package/dist/index.js +4 -4
  18. package/dist/{options-XTBpbx0s.d.ts → options-BlQ00Fju.d.cts} +3 -1
  19. package/dist/{options-XTBpbx0s.d.cts → options-BlQ00Fju.d.ts} +3 -1
  20. package/dist/{plugin-DlidwMj6.d.cts → plugin-DJR6z3e0.d.cts} +3 -3
  21. package/dist/{plugin-DP6A4t4o.d.ts → plugin-N_EaXN4-.d.ts} +3 -3
  22. package/dist/plugin.cjs +4 -0
  23. package/dist/plugin.d.cts +4 -4
  24. package/dist/plugin.d.ts +4 -4
  25. package/dist/plugin.js +2 -2
  26. package/dist/providers/binance.d.cts +2 -2
  27. package/dist/providers/binance.d.ts +2 -2
  28. package/dist/providers/coinbase.d.cts +2 -2
  29. package/dist/providers/coinbase.d.ts +2 -2
  30. package/dist/providers/hyperliquid.d.cts +2 -2
  31. package/dist/providers/hyperliquid.d.ts +2 -2
  32. package/dist/ui.cjs +1318 -4
  33. package/dist/ui.d.cts +383 -2
  34. package/dist/ui.d.ts +383 -2
  35. package/dist/ui.js +3 -3
  36. package/dist/vela.global.js +2709 -1600
  37. package/dist/vela.global.min.js +315 -66
  38. package/dist/widget.cjs +2275 -1032
  39. package/dist/widget.d.cts +25 -7
  40. package/dist/widget.d.ts +25 -7
  41. package/dist/widget.js +27 -9
  42. package/dist/workspace.cjs +2181 -926
  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"];
@@ -15301,6 +16774,12 @@ function extendEndpoints(x1, y1, x2, y2, extend, width, height) {
15301
16774
  const rx = right ? width + 2 : Math.max(x1, x2);
15302
16775
  return [lx, yAt(lx), rx, yAt(rx)];
15303
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
+ }
15304
16783
  function parseColor2(c) {
15305
16784
  const s = c.trim();
15306
16785
  let m = /^#([0-9a-f]{3})$/i.exec(s);
@@ -16555,380 +18034,71 @@ function tzButtonLabel(zone) {
16555
18034
  return tzOffset(zone);
16556
18035
  }
16557
18036
 
16558
- // src/core/drawings/effectiveColor.ts
16559
- var VALID_FILL = VALID;
16560
- var INVALID_FILL = INVALID;
16561
- function effectiveFillColor(d, theme) {
16562
- if (d instanceof PatternDrawing) {
16563
- if (!d.fillTriangles().length) return null;
16564
- const valid = d.valid();
16565
- return d.style.fillColor ?? (valid == null ? d.style.lineColor : valid ? VALID_FILL : INVALID_FILL);
16566
- }
16567
- if (d instanceof CalloutBase) return d.style.fillColor ?? theme.background;
16568
- return d.style.fillColor ?? null;
16569
- }
16570
-
16571
- // src/renderers/native/drawings/colorPicker.ts
16572
- function splitColor(color) {
16573
- const s = String(color ?? "").trim();
16574
- let m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
16575
- if (m) return { hex6: `#${m[1].toLowerCase()}`, alpha: m[2] ? parseInt(m[2], 16) / 255 : 1 };
16576
- m = /^#([0-9a-f]{3})$/i.exec(s);
16577
- if (m) {
16578
- const h = m[1].toLowerCase();
16579
- return { hex6: `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`, alpha: 1 };
16580
- }
16581
- const rgba = /^rgba?\(([^)]+)\)/i.exec(s);
16582
- if (rgba) {
16583
- const p = rgba[1].split(",").map((v) => v.trim());
16584
- const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
16585
- const a = p[3] != null ? Math.max(0, Math.min(1, parseFloat(p[3]))) : 1;
16586
- return { hex6: `#${hx(parseInt(p[0] ?? "0", 10))}${hx(parseInt(p[1] ?? "0", 10))}${hx(parseInt(p[2] ?? "0", 10))}`, alpha: a };
16587
- }
16588
- return { hex6: DEFAULT_DRAWING_COLOR, alpha: 1 };
16589
- }
16590
- function combineColor(hex6, alpha) {
16591
- const a = Math.max(0, Math.min(1, alpha));
16592
- if (a >= 0.999) return hex6;
16593
- return `${hex6}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
16594
- }
16595
- function blendOver(fg, bg, alpha) {
16596
- const rgb = (c) => {
16597
- const n = parseInt(splitColor(c).hex6.slice(1), 16);
16598
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
16599
- };
16600
- const [fr, fgc, fb] = rgb(fg);
16601
- const [br, bgc, bb] = rgb(bg);
16602
- const a = Math.max(0, Math.min(1, alpha));
16603
- const h = (n) => Math.round(n).toString(16).padStart(2, "0");
16604
- return `#${h(fr * a + br * (1 - a))}${h(fgc * a + bgc * (1 - a))}${h(fb * a + bb * (1 - a))}`;
16605
- }
16606
- function hslHex(h, s, l) {
16607
- const sn = s / 100;
16608
- const ln = l / 100;
16609
- const k = (n) => (n + h / 30) % 12;
16610
- const a = sn * Math.min(ln, 1 - ln);
16611
- const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
16612
- const to = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
16613
- return `#${to(f(0))}${to(f(8))}${to(f(4))}`;
16614
- }
16615
- function buildPalette() {
16616
- const grays = [100, 90, 80, 68, 56, 45, 35, 25, 14, 0].map((l) => hslHex(0, 0, l));
16617
- const hues = [4, 28, 50, 96, 140, 174, 200, 224, 270, 322];
16618
- const levels = [
16619
- [82, 56],
16620
- // vivid
16621
- [58, 84],
16622
- // light pastel
16623
- [62, 74],
16624
- [66, 62],
16625
- [70, 50],
16626
- [64, 39],
16627
- [54, 29]
16628
- // dark
16629
- ];
16630
- return [grays, ...levels.map(([s, l]) => hues.map((h) => hslHex(h, s, l)))];
16631
- }
16632
- var PALETTE = buildPalette();
16633
- function transparencyChecker(size) {
16634
- return `repeating-conic-gradient(#9aa0a6 0% 25%, #d3d6da 0% 50%) 0 0 / ${size}px ${size}px`;
16635
- }
16636
- var CHECKER = transparencyChecker(10);
16637
- var recents = [ACCENT, BULLISH, BEARISH, WARNING, NEUTRAL];
16638
- function addRecent(hex6) {
16639
- const i = recents.findIndex((c) => c.toLowerCase() === hex6.toLowerCase());
16640
- if (i >= 0) recents.splice(i, 1);
16641
- recents.unshift(hex6);
16642
- if (recents.length > 10) recents.length = 10;
16643
- }
16644
- function buildColorPicker(color, theme, onChange) {
16645
- const parsed = splitColor(color);
16646
- let curHex = parsed.hex6;
16647
- let curAlpha = parsed.alpha;
16648
- const root = document.createElement("div");
16649
- root.style.cssText = "display:flex;flex-direction:column;gap:9px;width:236px;";
16650
- const grid = document.createElement("div");
16651
- grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
16652
- const swatches = [];
16653
- for (const row of PALETTE) {
16654
- for (const c of row) {
16655
- const sw = document.createElement("button");
16656
- sw.type = "button";
16657
- sw.dataset.c = c;
16658
- sw.style.cssText = `width:100%;aspect-ratio:1;border-radius:4px;border:none;cursor:pointer;background:${c};padding:0;`;
16659
- sw.addEventListener("click", (e) => {
16660
- e.stopPropagation();
16661
- pickHex(c);
16662
- });
16663
- grid.appendChild(sw);
16664
- swatches.push(sw);
16665
- }
16666
- }
16667
- const paintSelection = () => {
16668
- for (const sw of swatches) {
16669
- const on = (sw.dataset.c ?? "").toLowerCase() === curHex.toLowerCase();
16670
- sw.style.outline = on ? `2px solid ${theme.textColor}` : "none";
16671
- sw.style.outlineOffset = "2px";
16672
- sw.style.zIndex = on ? "1" : "";
16673
- sw.style.position = on ? "relative" : "";
16674
- }
16675
- };
16676
- const recentRow = document.createElement("div");
16677
- recentRow.style.cssText = "display:flex;align-items:center;gap:5px;flex-wrap:wrap;border-top:1px solid var(--vela-border);padding-top:9px;";
16678
- const renderRecents = () => {
16679
- recentRow.replaceChildren();
16680
- for (const c of recents) {
16681
- const sw = document.createElement("button");
16682
- sw.type = "button";
16683
- sw.style.cssText = `width:17px;height:17px;border-radius:var(--vela-radius-sm);border:1px solid var(--vela-border);cursor:pointer;background:${c};padding:0;`;
16684
- sw.addEventListener("click", (e) => {
16685
- e.stopPropagation();
16686
- pickHex(c);
16687
- });
16688
- recentRow.appendChild(sw);
16689
- }
16690
- const add = document.createElement("label");
16691
- add.style.cssText = `width:17px;height:17px;border-radius:var(--vela-radius-sm);border:1px dashed var(--vela-border-strong);cursor:pointer;display:flex;align-items:center;justify-content:center;color:${theme.textColor};font:14px ${theme.fontFamily};position:relative;`;
16692
- add.textContent = "+";
16693
- const input = document.createElement("input");
16694
- input.type = "color";
16695
- input.value = curHex;
16696
- input.style.cssText = "position:absolute;inset:0;opacity:0;cursor:pointer;";
16697
- input.addEventListener("input", () => pickHex(input.value, true));
16698
- add.appendChild(input);
16699
- recentRow.appendChild(add);
16700
- };
16701
- const opLabel = document.createElement("div");
16702
- opLabel.textContent = "Opacity";
16703
- opLabel.style.cssText = `font:11px ${theme.fontFamily};color:var(--vela-fg-muted);`;
16704
- const opRow = document.createElement("div");
16705
- opRow.style.cssText = "display:flex;align-items:center;gap:10px;";
16706
- const track = document.createElement("div");
16707
- track.style.cssText = "flex:1;position:relative;height:13px;border-radius:7px;cursor:pointer;";
16708
- const knob = document.createElement("div");
16709
- knob.style.cssText = "position:absolute;top:50%;width:15px;height:15px;border-radius:50%;background:var(--vela-selected-bg);box-shadow:0 1px 3px rgba(0,0,0,0.55);transform:translate(-50%,-50%);pointer-events:none;";
16710
- track.appendChild(knob);
16711
- const pctBox = document.createElement("div");
16712
- pctBox.style.cssText = `min-width:42px;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;`;
16713
- opRow.append(track, pctBox);
16714
- const paintOpacity = () => {
16715
- track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
16716
- knob.style.left = `${curAlpha * 100}%`;
16717
- pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
16718
- };
16719
- let dragging = false;
16720
- const onDrag = (clientX) => {
16721
- const r = track.getBoundingClientRect();
16722
- curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
16723
- paintOpacity();
16724
- emit();
16725
- };
16726
- track.addEventListener("pointerdown", (e) => {
16727
- e.stopPropagation();
16728
- dragging = true;
16729
- track.setPointerCapture(e.pointerId);
16730
- onDrag(e.clientX);
16731
- });
16732
- track.addEventListener("pointermove", (e) => {
16733
- if (dragging) onDrag(e.clientX);
16734
- });
16735
- track.addEventListener("pointerup", () => dragging = false);
16736
- function emit() {
16737
- onChange(combineColor(curHex, curAlpha));
16738
- }
16739
- function pickHex(hex, custom = false) {
16740
- curHex = splitColor(hex).hex6;
16741
- addRecent(curHex);
16742
- paintSelection();
16743
- paintOpacity();
16744
- if (custom) renderRecents();
16745
- emit();
16746
- }
16747
- renderRecents();
16748
- paintSelection();
16749
- paintOpacity();
16750
- root.append(grid, recentRow, opLabel, opRow);
16751
- return root;
16752
- }
16753
-
16754
- // src/renderers/native/chrome/ColorField.ts
16755
- var CHECKER2 = transparencyChecker(8);
16756
- var STYLE_ID2 = "vela-color-field";
16757
- function ensureStyles2() {
16758
- if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
16759
- const st = document.createElement("style");
16760
- st.id = STYLE_ID2;
16761
- st.textContent = `
16762
- .vela-color-field{width:24px;height:24px;padding:2px;border:1px solid var(--vela-border);border-radius:0;background:var(--vela-surface-sunken);cursor:pointer;display:inline-flex;flex:none;}
16763
- .vela-color-field:hover{border-color:var(--vela-fg-muted);}
16764
- .vela-color-field-swatch{display:block;width:100%;height:100%;border-radius:0;box-shadow:inset 0 0 0 1px rgba(0,0,0,0.25);}
16765
- .vela-color-field-pop{position:fixed;z-index:6000;background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:10px;}
16766
- `;
16767
- document.head.appendChild(st);
16768
- }
16769
- var openPopover = null;
16770
- function closeOpen() {
16771
- if (!openPopover) return;
16772
- document.removeEventListener("pointerdown", openPopover.onOutside, true);
16773
- window.removeEventListener("resize", openPopover.reflow, true);
16774
- openPopover.el.remove();
16775
- openPopover = null;
16776
- }
16777
- function place(pop, trigger) {
16778
- const r = trigger.getBoundingClientRect();
16779
- const w = pop.offsetWidth;
16780
- const h = pop.offsetHeight;
16781
- let left = r.right - w;
16782
- let top = r.bottom + 6;
16783
- if (left < 6) left = 6;
16784
- if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
16785
- if (top + h > window.innerHeight - 6) top = r.top - h - 6;
16786
- if (top < 6) top = 6;
16787
- pop.style.left = `${Math.round(left)}px`;
16788
- pop.style.top = `${Math.round(top)}px`;
16789
- }
16790
- function colorField(theme, getVal, onVal) {
16791
- ensureStyles2();
16792
- const trigger = document.createElement("button");
16793
- trigger.type = "button";
16794
- trigger.className = "vela-color-field";
16795
- const swatch = document.createElement("span");
16796
- swatch.className = "vela-color-field-swatch";
16797
- trigger.appendChild(swatch);
16798
- const paint = () => {
16799
- const v = getVal();
16800
- swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
16801
- };
16802
- paint();
16803
- trigger.addEventListener("vela-sync", paint);
16804
- trigger.addEventListener("click", (e) => {
16805
- e.stopPropagation();
16806
- if (openPopover && openPopover.trigger === trigger) {
16807
- closeOpen();
16808
- return;
16809
- }
16810
- closeOpen();
16811
- const pop = document.createElement("div");
16812
- pop.className = "vela-color-field-pop";
16813
- applyChromeTokens(pop, theme);
16814
- pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
16815
- pop.appendChild(
16816
- buildColorPicker(getVal(), theme, (val) => {
16817
- onVal(val);
16818
- paint();
16819
- })
16820
- );
16821
- document.body.appendChild(pop);
16822
- place(pop, trigger);
16823
- const onOutside = (ev) => {
16824
- const t = ev.target;
16825
- if (pop.contains(t) || trigger.contains(t)) return;
16826
- closeOpen();
16827
- };
16828
- const reflow = () => place(pop, trigger);
16829
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
16830
- window.addEventListener("resize", reflow, true);
16831
- openPopover = { el: pop, trigger, onOutside, reflow };
16832
- });
16833
- return trigger;
16834
- }
16835
- function closeColorPopover() {
16836
- closeOpen();
16837
- }
16838
-
16839
18037
  // src/renderers/native/chrome/WidthField.ts
16840
18038
  var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
16841
- var STYLE_ID3 = "vela-width-field";
16842
- function ensureStyles3() {
16843
- if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
16844
- const st = document.createElement("style");
16845
- st.id = STYLE_ID3;
16846
- st.textContent = `
18039
+ var STYLE_ID2 = "vela-width-field";
18040
+ var WIDTH_CSS = `
16847
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;}
16848
18042
  .vela-width-field:hover{border-color:var(--vela-fg-muted);}
16849
- .vela-width-field-pop{position:fixed;z-index:6000;background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;display:flex;flex-direction:column;gap:1px;color:var(--vela-fg);}
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);}
16850
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;}
16851
18046
  .vela-width-field-item:hover{background:var(--vela-hover-strong);}
16852
18047
  .vela-width-field-item[data-active='1']{background:var(--vela-active);}
16853
18048
  `;
16854
- document.head.appendChild(st);
16855
- }
16856
18049
  function lineGlyph(width) {
16857
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>`;
16858
18051
  }
16859
- var CHEVRON = `<svg width="8" height="5" viewBox="0 0 8 5" fill="none"><path d="M1 1l3 3 3-3" stroke="#868a96" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
16860
- var openPopover2 = null;
16861
- function closeOpen2() {
16862
- if (!openPopover2) return;
16863
- document.removeEventListener("pointerdown", openPopover2.onOutside, true);
16864
- window.removeEventListener("resize", openPopover2.reflow, true);
16865
- openPopover2.el.remove();
16866
- openPopover2 = null;
16867
- }
16868
- function place2(pop, trigger) {
16869
- const r = trigger.getBoundingClientRect();
16870
- const w = pop.offsetWidth;
16871
- const h = pop.offsetHeight;
16872
- let left = r.right - w;
16873
- let top = r.bottom + 6;
16874
- if (left < 6) left = 6;
16875
- if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
16876
- if (top + h > window.innerHeight - 6) top = r.top - h - 6;
16877
- if (top < 6) top = 6;
16878
- pop.style.left = `${Math.round(left)}px`;
16879
- pop.style.top = `${Math.round(top)}px`;
16880
- }
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>`;
16881
18053
  function widthField(theme, getVal, onVal) {
16882
- ensureStyles3();
18054
+ injectStyles(STYLE_ID2, WIDTH_CSS, document);
16883
18055
  const trigger = document.createElement("button");
16884
18056
  trigger.type = "button";
16885
18057
  trigger.className = "vela-width-field";
16886
18058
  const paint = () => {
16887
- 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>`;
16888
18060
  };
16889
18061
  paint();
18062
+ trigger.addEventListener("pointerdown", (e) => {
18063
+ e.preventDefault();
18064
+ trigger.focus({ preventScroll: true });
18065
+ });
16890
18066
  trigger.addEventListener("click", (e) => {
16891
18067
  e.stopPropagation();
16892
- if (openPopover2 && openPopover2.trigger === trigger) {
16893
- closeOpen2();
18068
+ if (openPopoverTrigger() === trigger) {
18069
+ closeOpenPopovers();
16894
18070
  return;
16895
18071
  }
16896
- closeOpen2();
16897
- const pop = document.createElement("div");
16898
- pop.className = "vela-width-field-pop";
16899
- applyChromeTokens(pop, theme);
16900
- pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
16901
- pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
16902
- for (const w of WIDTH_FIELD_OPTIONS) {
16903
- const item = document.createElement("button");
16904
- item.type = "button";
16905
- item.className = "vela-width-field-item";
16906
- item.dataset.active = w === getVal() ? "1" : "0";
16907
- item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
16908
- item.addEventListener("click", (ev) => {
16909
- ev.stopPropagation();
16910
- closeOpen2();
16911
- onVal(w);
16912
- paint();
16913
- });
16914
- pop.appendChild(item);
16915
- }
16916
- document.body.appendChild(pop);
16917
- place2(pop, trigger);
16918
- const onOutside = (ev) => {
16919
- const t = ev.target;
16920
- if (pop.contains(t) || trigger.contains(t)) return;
16921
- closeOpen2();
16922
- };
16923
- const reflow = () => place2(pop, trigger);
16924
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
16925
- window.addEventListener("resize", reflow, true);
16926
- openPopover2 = { el: pop, trigger, onOutside, reflow };
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();
16927
18097
  });
16928
18098
  return trigger;
16929
18099
  }
16930
18100
  function closeWidthPopover() {
16931
- closeOpen2();
18101
+ closeOpenPopovers();
16932
18102
  }
16933
18103
 
16934
18104
  // src/renderers/native/chrome/SettingsDialog.ts
@@ -16943,37 +18113,24 @@ function styleLabel(id) {
16943
18113
  return chartType(id)?.label ?? BUILTIN_STYLE_LABELS[id] ?? id;
16944
18114
  }
16945
18115
  var SD_STYLE_ID = "vela-settings-controls";
18116
+ var SD_STYLE_REV = "2";
16946
18117
  var SETTINGS_SURFACE = "var(--vela-surface)";
16947
18118
  var SETTINGS_BORDER = "var(--vela-border)";
16948
18119
  var SETTINGS_TEXT = "var(--vela-fg)";
16949
18120
  function ensureControlStyles() {
16950
- if (typeof document === "undefined" || document.getElementById(SD_STYLE_ID)) return;
16951
- 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");
16952
18125
  st.id = SD_STYLE_ID;
18126
+ st.dataset.rev = SD_STYLE_REV;
16953
18127
  st.textContent = `
16954
- .vela-sd-check{width:18px;height:18px;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0;border:1px solid var(--vela-border-strong);border-radius:5px;background:transparent;color:transparent;cursor:pointer;}
16955
- .vela-sd-check:hover{border-color:var(--vela-fg-muted);}
16956
- .vela-sd-check.on{background:var(--vela-selected-bg);border-color:var(--vela-selected-bg);color:var(--vela-selected-fg);}
16957
- .vela-sd-check svg{display:block;}
16958
- .vela-sd-select,.vela-sd-number{height:28px;background:var(--vela-surface-elev);border:1px solid var(--vela-border-strong);border-radius:var(--vela-radius-sm);color:var(--vela-fg);padding:0 8px;font-size:13px;outline:none;font-family:inherit;}
16959
- .vela-sd-select:hover,.vela-sd-number:hover{border-color:var(--vela-fg-muted);}
16960
- /* Custom caret: the native one hugs the right border; ours gets 8px of air. (A data URI
16961
- can't read currentColor, so it uses the shared muted-gray ink, legible on both themes.) */
16962
- .vela-sd-select{-webkit-appearance:none;appearance:none;padding-right:26px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M1 1l3 3 3-3' fill='none' stroke='%23868a96' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;}
16963
- .vela-sd-number{width:64px;}
16964
- .vela-sd-color{width:32px;height:26px;padding:0;border:1px solid var(--vela-border);border-radius:var(--vela-radius-sm);background:transparent;cursor:pointer;-webkit-appearance:none;appearance:none;}
16965
- .vela-sd-color::-webkit-color-swatch-wrapper{padding:2px;}
16966
- .vela-sd-color::-webkit-color-swatch{border:none;border-radius:2px;}
16967
18128
  .vela-sd-pane::-webkit-scrollbar{width:8px;}
16968
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;}
16969
18130
  .vela-sd-pane::-webkit-scrollbar-track{background:transparent;}
16970
- .vela-sd-toggle{position:relative;width:38px;height:22px;border-radius:11px;background:var(--vela-surface-sunken);border:1px solid var(--vela-border);cursor:pointer;flex:none;padding:0;}
16971
- .vela-sd-toggle::after{content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--vela-fg-muted);transition:transform var(--vela-dur-med) ease,background var(--vela-dur-med) ease;}
16972
- .vela-sd-toggle.on{background:var(--vela-active);border-color:var(--vela-selected-bg);}
16973
- .vela-sd-toggle.on::after{transform:translateX(16px);background:var(--vela-selected-bg);}
16974
18131
  /* Tab rail / footer button / header close: base styles live HERE, not inline on the
16975
18132
  elements \u2014 inline declarations always beat stylesheet :hover rules, which is exactly
16976
- what killed these hovers before. Active tab state is the .on class (like .vela-sd-check),
18133
+ what killed these hovers before. Active tab state is the .on class,
16977
18134
  hover fills follow the app convention (--vela-hover + --vela-fg-bright, fast transition). */
16978
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;}
16979
18136
  .vela-sd-tab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
@@ -17032,13 +18189,13 @@ function ensureControlStyles() {
17032
18189
  .vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
17033
18190
  .vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
17034
18191
  .vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
17035
- .vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
17036
- .vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
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;}
17037
18194
  .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
17038
18195
  .vela-sd-mobile .vela-sd-btn{height:38px;}
17039
18196
  .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
17040
18197
  `;
17041
- document.head.appendChild(st);
18198
+ if (!existing) document.head.appendChild(st);
17042
18199
  }
17043
18200
  var SettingsDialog = class {
17044
18201
  constructor(container, theme) {
@@ -17354,15 +18511,15 @@ var SettingsDialog = class {
17354
18511
  railScrim = document.createElement("div");
17355
18512
  railScrim.className = "vela-sd-railscrim";
17356
18513
  railScrim.addEventListener("click", () => toggleRail?.());
17357
- toggleRail = (open) => {
17358
- const on = open ?? !rail.classList.contains("open");
18514
+ toggleRail = (open2) => {
18515
+ const on = open2 ?? !rail.classList.contains("open");
17359
18516
  rail.classList.toggle("open", on);
17360
18517
  railScrim?.classList.toggle("open", on);
17361
18518
  };
17362
18519
  }
17363
18520
  const paneHost = document.createElement("div");
17364
18521
  paneHost.className = "vela-sd-pane";
17365
- paneHost.style.cssText = mobile ? "flex:1;min-width:0;overflow-y:auto;padding:6px 14px calc(14px + env(safe-area-inset-bottom, 0px));" : "flex:1;overflow-y:auto;padding:6px 18px 14px;";
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;";
17366
18523
  const panes = [];
17367
18524
  let current = null;
17368
18525
  for (const child of [...body.children]) {
@@ -17428,7 +18585,7 @@ var SettingsDialog = class {
17428
18585
  }
17429
18586
  }
17430
18587
  close() {
17431
- closeColorPopover();
18588
+ closeOpenPopovers();
17432
18589
  closeWidthPopover();
17433
18590
  this.root?.remove();
17434
18591
  this.root = null;
@@ -17583,50 +18740,41 @@ var SettingsDialog = class {
17583
18740
  return wf;
17584
18741
  }
17585
18742
  if (c.kind === "select") {
17586
- const sel = document.createElement("select");
17587
- sel.className = "vela-sd-select";
17588
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
17589
- sel.title = c.label;
17590
18743
  const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
17591
- for (const [val, lbl] of normalizeSelectOptions(c.options)) {
17592
- const o = document.createElement("option");
17593
- o.value = val;
17594
- o.textContent = lbl;
17595
- if (val === current2) o.selected = true;
17596
- sel.appendChild(o);
17597
- }
17598
- sel.addEventListener("change", () => put(c.key, sel.value));
17599
- sel.addEventListener("vela-sync", () => {
17600
- sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
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
- return sel;
17603
- }
17604
- const ni = document.createElement("input");
17605
- ni.type = "number";
17606
- ni.className = "vela-sd-number";
17607
- if (compact) ni.style.width = "56px";
17608
- if (c.min !== void 0) ni.min = String(c.min);
17609
- if (c.max !== void 0) ni.max = String(c.max);
17610
- ni.step = String(c.step ?? 1);
17611
- ni.title = c.label;
17612
- const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
17613
- if (c.placeholder !== void 0) {
17614
- ni.placeholder = c.placeholder;
17615
- if (current !== c.defval) ni.value = String(current);
17616
- ni.addEventListener("change", () => {
17617
- const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
17618
- const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
17619
- ni.value = v === c.defval ? "" : String(v);
17620
- put(c.key, v);
17621
- });
17622
- } else {
17623
- ni.value = String(current);
17624
- ni.addEventListener("input", () => {
17625
- const v = Number(ni.value);
17626
- if (Number.isFinite(v)) put(c.key, v);
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);
17627
18754
  });
18755
+ return sel.el;
17628
18756
  }
17629
- 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;
17630
18778
  }
17631
18779
  /**
17632
18780
  * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
@@ -17907,28 +19055,27 @@ var SettingsDialog = class {
17907
19055
  * only one visible at a time; see `typeRow`). */
17908
19056
  toggleRow(label, value, onToggle, controls, get) {
17909
19057
  const wrap = document.createElement("div");
17910
- const cb = document.createElement("button");
17911
- cb.type = "button";
17912
- cb.className = "vela-sd-check" + (value ? " on" : "");
17913
- cb.innerHTML = iconAt("check", 11);
17914
- let checked = value;
17915
- const cbToggle = () => {
17916
- checked = !checked;
17917
- cb.classList.toggle("on", checked);
17918
- return checked;
17919
- };
19058
+ const sw = new Switch({ checked: value, size: "sm", onChange: (v) => {
19059
+ onToggle(v);
19060
+ if (controls.length > 0) syncDim(v);
19061
+ } });
17920
19062
  const lbl = document.createElement("span");
17921
19063
  lbl.textContent = label;
17922
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
+ };
17923
19072
  if (controls.length === 0) {
17924
19073
  wrap.className = "vela-sd-bool";
17925
19074
  wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
17926
- wrap.append(cb, lbl);
17927
- cb.addEventListener("click", () => onToggle(cbToggle()));
19075
+ wrap.append(sw.el, lbl);
17928
19076
  if (get) {
17929
19077
  wrap.addEventListener("vela-sync", () => {
17930
- checked = get();
17931
- cb.classList.toggle("on", checked);
19078
+ sw.setChecked(get());
17932
19079
  });
17933
19080
  }
17934
19081
  return wrap;
@@ -17936,28 +19083,16 @@ var SettingsDialog = class {
17936
19083
  wrap.className = "vela-sd-row";
17937
19084
  const left = document.createElement("div");
17938
19085
  left.style.cssText = "display:flex;align-items:center;gap:8px;";
17939
- left.append(cb, lbl);
19086
+ left.append(sw.el, lbl);
17940
19087
  const box = document.createElement("div");
17941
19088
  box.style.cssText = "display:flex;align-items:center;gap:6px;";
17942
19089
  for (const c of controls) box.appendChild(c);
17943
- const syncDim = (on) => {
17944
- for (const c of controls) {
17945
- if (c.dataset.sdSelfGated === "1") continue;
17946
- c.style.opacity = on ? "1" : "0.4";
17947
- c.style.pointerEvents = on ? "" : "none";
17948
- }
17949
- };
17950
19090
  syncDim(value);
17951
- cb.addEventListener("click", () => {
17952
- const v = cbToggle();
17953
- onToggle(v);
17954
- syncDim(v);
17955
- });
17956
19091
  if (get) {
17957
19092
  wrap.addEventListener("vela-sync", () => {
17958
- checked = get();
17959
- cb.classList.toggle("on", checked);
17960
- syncDim(checked);
19093
+ const v = get();
19094
+ sw.setChecked(v);
19095
+ syncDim(v);
17961
19096
  });
17962
19097
  }
17963
19098
  wrap.append(left, box);
@@ -17965,52 +19100,23 @@ var SettingsDialog = class {
17965
19100
  }
17966
19101
  numberRow(label, value, min, max, step, onChange) {
17967
19102
  const { wrap } = this.row(label);
17968
- const ni = document.createElement("input");
17969
- ni.type = "number";
17970
- ni.value = String(value);
17971
- ni.min = String(min);
17972
- ni.max = String(max);
17973
- ni.step = String(step);
17974
- ni.className = "vela-sd-number";
17975
- ni.addEventListener("input", () => {
17976
- const n = Number(ni.value);
17977
- if (Number.isFinite(n)) onChange(n);
17978
- });
17979
- wrap.appendChild(ni);
19103
+ wrap.appendChild(new NumberInput({ value, min, max, step, size: "sm", commit: "live", onChange }).el);
17980
19104
  return wrap;
17981
19105
  }
17982
19106
  /** A dropdown whose option values differ from their display labels. */
17983
19107
  selectRowLabeled(label, value, options, onChange) {
17984
19108
  const { wrap } = this.row(label);
17985
- const sel = document.createElement("select");
17986
- sel.className = "vela-sd-select";
17987
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
17988
- for (const [val, lbl] of options) {
17989
- const o = document.createElement("option");
17990
- o.value = val;
17991
- o.textContent = lbl;
17992
- if (val === value) o.selected = true;
17993
- sel.appendChild(o);
17994
- }
17995
- sel.addEventListener("change", () => onChange(sel.value));
17996
- wrap.appendChild(sel);
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);
17997
19116
  return wrap;
17998
19117
  }
17999
19118
  selectRow(label, value, options, onChange) {
18000
- const { wrap } = this.row(label);
18001
- const sel = document.createElement("select");
18002
- sel.className = "vela-sd-select";
18003
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
18004
- for (const opt of options) {
18005
- const o = document.createElement("option");
18006
- o.value = opt;
18007
- o.textContent = opt;
18008
- if (opt === value) o.selected = true;
18009
- sel.appendChild(o);
18010
- }
18011
- sel.addEventListener("change", () => onChange(sel.value));
18012
- wrap.appendChild(sel);
18013
- return wrap;
19119
+ return this.selectRowLabeled(label, value, options.map((o) => [o, o]), onChange);
18014
19120
  }
18015
19121
  /** Footer with the full config as JSON — the export/import (templating) surface. */
18016
19122
  footer(_config) {
@@ -18035,6 +19141,19 @@ function timezoneOptions(current) {
18035
19141
  return [[normalized, tzMenuLabel(normalized, normalized)], ...options];
18036
19142
  }
18037
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
+
18038
19157
  // src/renderers/native/drawings/DrawingPainter.ts
18039
19158
  var HANDLE_RADIUS = 4.5;
18040
19159
  var HANDLE_BORDER = DEFAULT_DRAWING_COLOR;
@@ -18401,13 +19520,19 @@ var DrawingPainter = class {
18401
19520
  }
18402
19521
  this.paintLabel(ctx, d, proj, theme);
18403
19522
  }
18404
- /** 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. */
18405
19525
  paintLabel(ctx, d, proj, theme) {
18406
19526
  const text = d.text;
18407
19527
  if (!text || !text.value || d.id === this.targets.mutedLabel) return;
18408
19528
  const layout = labelLayout(d, proj);
18409
19529
  if (!layout) return;
18410
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
+ }
18411
19536
  ctx.font = `${text.bold ? "bold " : ""}${text.italic ? "italic " : ""}${fs}px ${theme.fontFamily}`;
18412
19537
  ctx.textBaseline = "top";
18413
19538
  ctx.textAlign = layout.align;
@@ -18415,8 +19540,8 @@ var DrawingPainter = class {
18415
19540
  const lh = labelLineHeight(fs);
18416
19541
  const lines = text.value.split("\n");
18417
19542
  lines.forEach((line, i) => ctx.fillText(line, layout.x, layout.top + i * lh));
18418
- ctx.textAlign = "left";
18419
19543
  if (d.type === "text") this.paintTextFrame(ctx, d.id, layout.x, layout.top, lines, fs, theme);
19544
+ ctx.restore();
18420
19545
  }
18421
19546
  /** Frame a plain text label while it is the target, so the text reads as a clickable object even
18422
19547
  * though it has no shape of its own: a firm frame once selected, a fainter one under the cursor.
@@ -19359,7 +20484,14 @@ function labelLayout(d, proj) {
19359
20484
  case "infoline":
19360
20485
  case "trendangle": {
19361
20486
  if (pts.length < 2) return null;
19362
- return { x: (pts[0][0] + pts[1][0]) / 2, top: (pts[0][1] + pts[1][1]) / 2 - 6 - lh * lines, align: "center" };
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
+ };
19363
20495
  }
19364
20496
  case "box": {
19365
20497
  if (pts.length < 2) return null;
@@ -19708,11 +20840,11 @@ function applyFree(anchor, pt, free) {
19708
20840
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
19709
20841
  var BTN = 30;
19710
20842
  var ICON = 21;
19711
- var STYLE_ID4 = "vela-drawing-popup-styles";
19712
- function ensureStyles4() {
19713
- 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;
19714
20846
  const s = document.createElement("style");
19715
- s.id = STYLE_ID4;
20847
+ s.id = STYLE_ID3;
19716
20848
  s.textContent = `
19717
20849
  .vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
19718
20850
  .vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -19746,17 +20878,14 @@ var DrawingSettingsPopup = class {
19746
20878
  this.textPanel = null;
19747
20879
  this.levelsPanel = null;
19748
20880
  this.colorPop = null;
19749
- // floating RGB + opacity picker for a color swatch
19750
20881
  this.colorOwner = null;
19751
- // the swatch the open color popover belongs to (for toggle)
19752
- this.menuEl = null;
19753
- // floating option list for a dropdown control
20882
+ this.menuPop = null;
19754
20883
  this.menuOwner = null;
19755
20884
  this.onClose = null;
19756
20885
  this.onOutside = (e) => {
19757
20886
  const target = e.target;
19758
- if (this.colorPop?.contains(target)) return;
19759
- if (this.menuEl?.contains(target)) return;
20887
+ if (this.colorPop?.el.contains(target)) return;
20888
+ if (this.menuPop?.el.contains(target)) return;
19760
20889
  if (this.el && !this.el.contains(target)) {
19761
20890
  const cb = this.onClose;
19762
20891
  this.close();
@@ -19775,13 +20904,13 @@ var DrawingSettingsPopup = class {
19775
20904
  * picker / dropdown menu), which live outside `el`. */
19776
20905
  contains(node) {
19777
20906
  if (!node) return false;
19778
- 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;
19779
20908
  }
19780
20909
  /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
19781
20910
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
19782
20911
  open(drawing, anchor, actions, onClose) {
19783
20912
  this.close();
19784
- ensureStyles4();
20913
+ ensureStyles2();
19785
20914
  this.onClose = onClose ?? null;
19786
20915
  const t = this.theme;
19787
20916
  const schema = drawing.schema();
@@ -19792,8 +20921,7 @@ var DrawingSettingsPopup = class {
19792
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;`;
19793
20922
  applyChromeTokens(el, t);
19794
20923
  el.addEventListener("pointerdown", () => {
19795
- this.closeColorPopover();
19796
- this.closeMenu();
20924
+ closeOpenPopovers();
19797
20925
  });
19798
20926
  const bar = document.createElement("div");
19799
20927
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
@@ -20530,49 +21658,76 @@ var DrawingSettingsPopup = class {
20530
21658
  });
20531
21659
  return b;
20532
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
+ }
20533
21703
  /** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
20534
21704
  openActionMenu(anchor, rows) {
20535
- this.closeMenu();
20536
- this.closeColorPopover();
20537
- const t = this.theme;
20538
- const menu2 = document.createElement("div");
20539
- menu2.className = "vela-dpop";
20540
- menu2.style.cssText = `position:absolute;z-index:26;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;pointer-events:auto;display:flex;flex-direction:column;gap:1px;color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};`;
20541
- applyChromeTokens(menu2, t);
20542
- menu2.addEventListener("pointerdown", (e) => e.stopPropagation());
20543
- for (const row of rows) {
20544
- const item = document.createElement("button");
20545
- item.type = "button";
20546
- item.className = "vela-dpop-item";
20547
- item.style.cssText = "display:flex;align-items:center;gap:9px;min-width:150px;padding:6px 9px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;";
20548
- const ic = document.createElement("span");
20549
- ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
20550
- ic.innerHTML = sized(row.icon, 18);
20551
- const tx = document.createElement("span");
20552
- tx.textContent = row.label;
20553
- tx.style.cssText = "flex:1;";
20554
- item.append(ic, tx);
20555
- item.addEventListener("click", (e) => {
20556
- e.stopPropagation();
20557
- this.closeMenu();
20558
- row.onClick();
20559
- });
20560
- menu2.appendChild(item);
20561
- }
20562
- this.host.appendChild(menu2);
20563
- const ar = anchor.getBoundingClientRect();
20564
- const hr = this.host.getBoundingClientRect();
20565
- const w = menu2.offsetWidth;
20566
- const h = menu2.offsetHeight;
20567
- let left = ar.right - hr.left - w;
20568
- let top = ar.bottom - hr.top + 6;
20569
- if (left + w > hr.width - 4) left = hr.width - w - 4;
20570
- if (left < 4) left = 4;
20571
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
20572
- if (top < 4) top = 4;
20573
- menu2.style.left = `${Math.round(left)}px`;
20574
- menu2.style.top = `${Math.round(top)}px`;
20575
- this.menuEl = menu2;
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
+ });
20576
21731
  this.menuOwner = anchor;
20577
21732
  }
20578
21733
  /** The floating hover-label. Sits just above or below the toolbar depending on where it floats,
@@ -20702,60 +21857,40 @@ var DrawingSettingsPopup = class {
20702
21857
  /** The floating option list opened by a {@link dropdown} trigger. Each row shows the option's
20703
21858
  * glyph (and text label when one is supplied); the current value is highlighted. */
20704
21859
  openMenu(anchor, values, current, render, label, onPick) {
20705
- this.closeMenu();
20706
- this.closeColorPopover();
20707
- const t = this.theme;
20708
- const menu2 = document.createElement("div");
20709
- menu2.className = "vela-dpop";
20710
- menu2.style.cssText = `position:absolute;z-index:26;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;pointer-events:auto;display:flex;flex-direction:column;gap:1px;color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};`;
20711
- applyChromeTokens(menu2, t);
20712
- menu2.addEventListener("pointerdown", (e) => e.stopPropagation());
20713
- for (const v of values) {
20714
- const active = v === current;
20715
- const item = document.createElement("button");
20716
- item.type = "button";
20717
- item.className = "vela-dpop-item";
20718
- item.dataset.active = active ? "1" : "0";
20719
- item.style.cssText = `display:flex;align-items:center;gap:8px;${label ? "min-width:118px;" : ""}padding:5px 8px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;`;
20720
- const ic = document.createElement("span");
20721
- ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
20722
- ic.innerHTML = sized(render(v), 18);
20723
- item.appendChild(ic);
20724
- if (label) {
20725
- const tx = document.createElement("span");
20726
- tx.textContent = label(v);
20727
- tx.style.cssText = "flex:1;font-variant-numeric:tabular-nums;";
20728
- item.appendChild(tx);
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
+ }
20729
21888
  }
20730
- item.addEventListener("click", (e) => {
20731
- e.stopPropagation();
20732
- this.closeMenu();
20733
- onPick(v);
20734
- });
20735
- menu2.appendChild(item);
20736
- }
20737
- this.host.appendChild(menu2);
20738
- const ar = anchor.getBoundingClientRect();
20739
- const hr = this.host.getBoundingClientRect();
20740
- const w = menu2.offsetWidth;
20741
- const h = menu2.offsetHeight;
20742
- let left = ar.left - hr.left;
20743
- let top = ar.bottom - hr.top + 6;
20744
- if (left + w > hr.width - 4) left = hr.width - w - 4;
20745
- if (left < 4) left = 4;
20746
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
20747
- if (top < 4) top = 4;
20748
- menu2.style.left = `${Math.round(left)}px`;
20749
- menu2.style.top = `${Math.round(top)}px`;
20750
- this.menuEl = menu2;
21889
+ });
20751
21890
  this.menuOwner = anchor;
20752
21891
  }
20753
21892
  closeMenu() {
20754
- if (this.menuEl) {
20755
- this.menuEl.remove();
20756
- this.menuEl = null;
20757
- }
20758
- this.menuOwner = null;
21893
+ this.menuPop?.hide();
20759
21894
  }
20760
21895
  /** A color control: an `icon` over a thin **colored underline** showing the current
20761
21896
  * value (the common idiom), with an invisible native picker overlaid to edit it.
@@ -20794,36 +21929,18 @@ var DrawingSettingsPopup = class {
20794
21929
  }
20795
21930
  /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
20796
21931
  openColorPopover(anchor, color, onChange) {
20797
- this.closeColorPopover();
20798
- this.closeMenu();
20799
21932
  const t = this.theme;
20800
- const pop = document.createElement("div");
20801
- pop.style.cssText = `position:absolute;z-index:25;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:10px;pointer-events:auto;`;
20802
- applyChromeTokens(pop, t);
20803
- pop.addEventListener("pointerdown", (e) => e.stopPropagation());
20804
- pop.appendChild(buildColorPicker(color, t, onChange));
20805
- this.host.appendChild(pop);
20806
- const ar = anchor.getBoundingClientRect();
20807
- const hr = this.host.getBoundingClientRect();
20808
- const w = pop.offsetWidth;
20809
- const h = pop.offsetHeight;
20810
- let left = ar.left - hr.left;
20811
- let top = ar.bottom - hr.top + 6;
20812
- if (left + w > hr.width - 4) left = hr.width - w - 4;
20813
- if (left < 4) left = 4;
20814
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
20815
- if (top < 4) top = 4;
20816
- pop.style.left = `${Math.round(left)}px`;
20817
- pop.style.top = `${Math.round(top)}px`;
20818
- this.colorPop = pop;
21933
+ this.colorPop = this.hostFloat(anchor, {
21934
+ zIndex: 25,
21935
+ padding: "10px",
21936
+ fill: (el) => {
21937
+ el.appendChild(buildColorPicker(color, t, onChange));
21938
+ }
21939
+ });
20819
21940
  this.colorOwner = anchor;
20820
21941
  }
20821
21942
  closeColorPopover() {
20822
- if (this.colorPop) {
20823
- this.colorPop.remove();
20824
- this.colorPop = null;
20825
- }
20826
- this.colorOwner = null;
21943
+ this.colorPop?.hide();
20827
21944
  }
20828
21945
  divider() {
20829
21946
  const d = document.createElement("div");
@@ -20962,7 +22079,7 @@ var DrawingToolbar = class {
20962
22079
  this.onCollapse = options.onCollapse ?? (() => {
20963
22080
  });
20964
22081
  this.onDrawingsSync = options.onDrawingsSync ?? null;
20965
- ensureStyles5();
22082
+ ensureStyles3();
20966
22083
  this.root = document.createElement("div");
20967
22084
  this.root.className = "vela-dtb";
20968
22085
  this.styleRoot();
@@ -21445,13 +22562,13 @@ var DrawingToolbar = class {
21445
22562
  }
21446
22563
  };
21447
22564
  var MAGNET_ID = "__magnet__";
21448
- var STYLE_ID5 = "vela-drawing-toolbar-styles";
21449
- function ensureStyles5() {
22565
+ var STYLE_ID4 = "vela-drawing-toolbar-styles";
22566
+ function ensureStyles3() {
21450
22567
  if (typeof document === "undefined") return;
21451
- let s = document.getElementById(STYLE_ID5);
22568
+ let s = document.getElementById(STYLE_ID4);
21452
22569
  if (!s) {
21453
22570
  s = document.createElement("style");
21454
- s.id = STYLE_ID5;
22571
+ s.id = STYLE_ID4;
21455
22572
  document.head.appendChild(s);
21456
22573
  }
21457
22574
  s.textContent = `
@@ -21830,9 +22947,9 @@ var UserDrawingController = class {
21830
22947
  }
21831
22948
  /**
21832
22949
  * Union of the visible drawings' price ranges on `paneId` whose time extent
21833
- * intersects [fromTime, toTime] folded into the pane's autoscale so an
21834
- * off-series drawing still expands the scale (mirrors Pine drawings). Hidden
21835
- * drawings and full-width references (hline null extent) are handled too.
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.
21836
22953
  */
21837
22954
  priceRangeForPane(paneId, fromTime, toTime) {
21838
22955
  let lo = Infinity;
@@ -22707,7 +23824,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
22707
23824
 
22708
23825
  // src/renderers/native/chrome/AttributionMark.ts
22709
23826
  var ATTRIBUTION_URL = "https://luxalgo.com/vela";
22710
- var STYLE_ID6 = "vela-attribution-styles";
23827
+ var STYLE_ID5 = "vela-attribution-styles";
22711
23828
  var CSS2 = `
22712
23829
  .vela-attribution {
22713
23830
  text-decoration: none;
@@ -22770,11 +23887,11 @@ var CSS2 = `
22770
23887
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
22771
23888
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
22772
23889
  `;
22773
- function ensureStyles6(doc) {
22774
- let s = doc.getElementById(STYLE_ID6);
23890
+ function ensureStyles4(doc) {
23891
+ let s = doc.getElementById(STYLE_ID5);
22775
23892
  if (!s) {
22776
23893
  s = doc.createElement("style");
22777
- s.id = STYLE_ID6;
23894
+ s.id = STYLE_ID5;
22778
23895
  doc.head.appendChild(s);
22779
23896
  }
22780
23897
  s.textContent = CSS2;
@@ -22803,7 +23920,7 @@ function createCustomMark(doc, html, background) {
22803
23920
  return el;
22804
23921
  }
22805
23922
  function createAttributionMark(doc, background) {
22806
- ensureStyles6(doc);
23923
+ ensureStyles4(doc);
22807
23924
  const a = doc.createElement("a");
22808
23925
  a.className = "vela-attribution";
22809
23926
  a.href = ATTRIBUTION_URL;
@@ -25781,7 +26898,6 @@ var NativeRenderer = class {
25781
26898
  const n = this.coords.barCount;
25782
26899
  if (n === 0) return;
25783
26900
  const vr = this.coords.visibleLogicalRange();
25784
- const vtr = this.coords.visibleTimeRange();
25785
26901
  const i0 = Math.max(0, Math.floor(vr.from));
25786
26902
  const i1 = Math.min(n - 1, Math.ceil(vr.to));
25787
26903
  if (i1 < i0) return;
@@ -25799,9 +26915,7 @@ var NativeRenderer = class {
25799
26915
  }
25800
26916
  const models = this.scene.indicatorsForPane(pane.id);
25801
26917
  const masterModels = models.filter((m) => m.ownScale !== true);
25802
- let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
25803
- const udr = this.userDrawings?.priceRangeForPane(pane.id, vtr.from, vtr.to) ?? null;
25804
- if (udr) dr = dr ? { min: Math.min(dr.min, udr.min), max: Math.max(dr.max, udr.max) } : udr;
26918
+ const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
25805
26919
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
25806
26920
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
25807
26921
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -26798,55 +27912,6 @@ function resolveElement(container) {
26798
27912
  if (!element) throw new Error(`[vela] container not found for selector "${container}"`);
26799
27913
  return element;
26800
27914
  }
26801
-
26802
- // src/ui/styles.ts
26803
- function injectStyles(id, css, root = document) {
26804
- if (typeof document === "undefined") return;
26805
- const host = root instanceof Document ? root.head : root;
26806
- if (root.getElementById?.(id) ?? host.querySelector(`#${CSS.escape(id)}`)) return;
26807
- const s = document.createElement("style");
26808
- s.id = id;
26809
- s.textContent = css;
26810
- host.appendChild(s);
26811
- }
26812
-
26813
- // src/ui/tokens.ts
26814
- var STATIC_ID = "vela-ui-tokens";
26815
- var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
26816
- var STATIC_CSS = `
26817
- .vela-ui, .vela-ui-layer {
26818
- ${STATIC_DECLS}
26819
- font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
26820
- box-sizing: border-box;
26821
- }
26822
- .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
26823
- .vela-icon { display: inline-flex; align-items: center; flex: none; }
26824
- .vela-icon svg { display: block; }
26825
- `;
26826
- function applyThemeTokens(el, t) {
26827
- const tokens = themeTokens(t);
26828
- for (const key in tokens) el.style.setProperty(key, tokens[key]);
26829
- }
26830
- function applyPlotOverlayTokens(host, base, config) {
26831
- const layout = config?.layout;
26832
- const t = layout?.background && layout.textColor ? { ...base, background: layout.background, textColor: layout.textColor } : base;
26833
- applyThemeTokens(host, t);
26834
- }
26835
- function ensureUIHost(el, theme) {
26836
- injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
26837
- el.classList.add("vela-ui");
26838
- if (theme) applyThemeTokens(el, theme);
26839
- }
26840
-
26841
- // src/ui/icons.ts
26842
- function iconEl(id, doc = document) {
26843
- const span = doc.createElement("span");
26844
- span.className = "vela-icon";
26845
- span.setAttribute("aria-hidden", "true");
26846
- const svg = iconMarkup(id);
26847
- if (svg) span.innerHTML = svg;
26848
- return span;
26849
- }
26850
27915
  function runMachine(machine5, props, render) {
26851
27916
  const m = new vanilla.VanillaMachine(machine5, props);
26852
27917
  const unsub = m.subscribe(render);
@@ -27088,7 +28153,10 @@ function menuController(opts) {
27088
28153
  props: {
27089
28154
  id: opts.id ?? nextUid("vela-menu"),
27090
28155
  ids: opts.triggerId ? { trigger: opts.triggerId } : void 0,
27091
- positioning: { placement: opts.placement ?? "bottom-start" },
28156
+ positioning: {
28157
+ placement: opts.placement ?? "bottom-start",
28158
+ ...opts.getAnchorRect ? { getAnchorRect: () => opts.getAnchorRect() } : {}
28159
+ },
27092
28160
  onSelect: (d) => opts.onSelect?.(d.value),
27093
28161
  onOpenChange: (d) => opts.onOpenChange?.(d.open)
27094
28162
  },
@@ -27170,6 +28238,22 @@ var MENU_CSS = `
27170
28238
  }
27171
28239
  .vela-menu-switch.on { background: var(--vela-accent-bright, var(--vela-accent)); border-color: var(--vela-accent-bright, var(--vela-accent)); }
27172
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); }
27173
28257
  .vela-menu-sep { height: 1px; margin: 4px 6px; background: var(--vela-border); }
27174
28258
  /* Submenu trigger row: a right-aligned chevron, and it stays highlighted while its own
27175
28259
  list is open so the path you came down remains readable. */
@@ -27188,9 +28272,13 @@ var Surface = class _Surface {
27188
28272
  this.items = [];
27189
28273
  /** Branch item id → the surface it opens. */
27190
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;
27191
28278
  this.doc = doc;
27192
28279
  this.host = opts.host;
27193
28280
  this.onSelect = opts.onSelect;
28281
+ this.onFavorite = opts.onFavorite;
27194
28282
  this.positioner = doc.createElement("div");
27195
28283
  this.positioner.className = "vela-ui-layer";
27196
28284
  this.list = doc.createElement("ul");
@@ -27198,16 +28286,25 @@ var Surface = class _Surface {
27198
28286
  if (opts.minWidth) this.list.style.minWidth = opts.minWidth;
27199
28287
  this.positioner.appendChild(this.list);
27200
28288
  this.host.appendChild(this.positioner);
28289
+ const trigger = opts.trigger;
27201
28290
  this.ctrl = menuController({
27202
28291
  items: [],
27203
28292
  id: opts.id,
27204
28293
  placement: opts.placement,
27205
28294
  onSelect: (id) => this.onSelect(id),
27206
- 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
+ },
27207
28305
  triggerId: opts.triggerId
27208
28306
  });
27209
28307
  this.mid = String(this.ctrl.props.id);
27210
- const trigger = opts.trigger;
27211
28308
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
27212
28309
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
27213
28310
  const api = this.ctrl.connect(service);
@@ -27287,6 +28384,25 @@ var Surface = class _Surface {
27287
28384
  hint.textContent = item.hint;
27288
28385
  li.appendChild(hint);
27289
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
+ }
27290
28406
  if (item.toggle) {
27291
28407
  const sw = doc.createElement("span");
27292
28408
  sw.className = "vela-menu-switch" + (item.checked ? " on" : "");
@@ -27302,6 +28418,7 @@ var Surface = class _Surface {
27302
28418
  host: this.host,
27303
28419
  placement: "right-start",
27304
28420
  onSelect: this.onSelect,
28421
+ onFavorite: this.onFavorite,
27305
28422
  id: `${this.mid}--${item.id}`
27306
28423
  });
27307
28424
  sub.setItems(item.submenu ?? []);
@@ -27325,7 +28442,8 @@ var Menu = class {
27325
28442
  trigger: opts.trigger,
27326
28443
  triggerId: opts.triggerId,
27327
28444
  id: opts.id,
27328
- minWidth: opts.minWidth
28445
+ minWidth: opts.minWidth,
28446
+ onFavorite: opts.onFavorite
27329
28447
  });
27330
28448
  this.root.setItems(opts.items);
27331
28449
  }
@@ -27829,7 +28947,7 @@ var Drawer = class {
27829
28947
  };
27830
28948
 
27831
28949
  // src/widget/layout-picker.ts
27832
- var STYLE_ID7 = "vela-widget-layout-picker-v14";
28950
+ var STYLE_ID6 = "vela-widget-layout-picker-v14";
27833
28951
  var CSS3 = `
27834
28952
  .vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
27835
28953
  .vela-lp {
@@ -27934,7 +29052,7 @@ var CSS3 = `
27934
29052
  `;
27935
29053
  var GRID = 4;
27936
29054
  function layoutGridCanvas(doc) {
27937
- injectStyles(STYLE_ID7, CSS3, doc);
29055
+ injectStyles(STYLE_ID6, CSS3, doc);
27938
29056
  const el = doc.createElement("div");
27939
29057
  el.className = "vela-lp-grid";
27940
29058
  const squares = [];
@@ -27974,7 +29092,7 @@ var LayoutPicker = class {
27974
29092
  };
27975
29093
  this.opts = opts;
27976
29094
  this.doc = opts.host.ownerDocument;
27977
- injectStyles(STYLE_ID7, CSS3, this.doc);
29095
+ injectStyles(STYLE_ID6, CSS3, this.doc);
27978
29096
  const doc = this.doc;
27979
29097
  this.layer = doc.createElement("div");
27980
29098
  this.layer.className = "vela-ui-layer vela-lp-layer";
@@ -28287,6 +29405,24 @@ function timeframeMs(value) {
28287
29405
  if (!Number.isFinite(count) || count < 1) return NaN;
28288
29406
  return count * UNIT_MS[key];
28289
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
+ }
28290
29426
  function timeframeLabel(value) {
28291
29427
  const parsed = parseTimeframe(value);
28292
29428
  if (!parsed.valid) return value;
@@ -28301,7 +29437,7 @@ function timeframeLabel(value) {
28301
29437
  }
28302
29438
 
28303
29439
  // src/widget/topbar.ts
28304
- var STYLE_ID8 = "vela-topbar";
29440
+ var STYLE_ID7 = "vela-topbar";
28305
29441
  var CSS4 = `
28306
29442
  .vela-widget-topbar {
28307
29443
  display: flex;
@@ -28339,6 +29475,36 @@ var CSS4 = `
28339
29475
  color: var(--vela-fg-bright);
28340
29476
  }
28341
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; }
28342
29508
  .vela-widget-topbar .vela-icon { color: inherit; font-size: 16px; width: 16px; height: 16px; }
28343
29509
  /* Width is set in syncHairlines() to exactly one device pixel \u2014 a CSS 1px at
28344
29510
  fractional DPR (1.25, 1.5\u2026) straddles two physical pixels and siblings end
@@ -28436,16 +29602,23 @@ var Topbar = class {
28436
29602
  this.timeframe = opts.timeframe;
28437
29603
  this.priceStyle = opts.priceStyle;
28438
29604
  const doc = host.ownerDocument;
28439
- injectStyles(STYLE_ID8, CSS4, doc);
29605
+ injectStyles(STYLE_ID7, CSS4, doc);
28440
29606
  this.el = doc.createElement("div");
28441
29607
  this.el.className = "vela-widget-topbar";
28442
29608
  this.symbolEl = doc.createElement("button");
28443
29609
  this.symbolEl.className = "vela-widget-symbol";
28444
29610
  this.symbolEl.textContent = parseSymbol(opts.symbol).ticker;
28445
29611
  if (opts.onSymbolClick) this.symbolEl.addEventListener("click", opts.onSymbolClick);
28446
- this.tfButton = doc.createElement("button");
28447
- this.tfButton.className = "vela-widget-tf";
28448
- this.tfButton.textContent = timeframeLabel(this.timeframe);
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);
28449
29622
  this.styleButton = doc.createElement("button");
28450
29623
  this.styleButton.className = "vela-widget-style";
28451
29624
  this.renderStyleButton(doc);
@@ -28485,19 +29658,20 @@ var Topbar = class {
28485
29658
  d.className = "vela-sep";
28486
29659
  return d;
28487
29660
  };
28488
- const leading = [this.symbolEl, sep(), this.tfButton, sep(), this.styleButton, sep()];
29661
+ const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
28489
29662
  if (this.layoutButton) leading.push(this.layoutButton, sep());
28490
29663
  if (indicatorsBtn) leading.push(indicatorsBtn, sep());
28491
29664
  this.leftActionsSep = sep();
28492
29665
  this.leftActionsSep.hidden = true;
28493
29666
  this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
28494
29667
  host.appendChild(this.el);
29668
+ this.renderTfChips();
28495
29669
  this.onHairlineSync();
28496
29670
  this.hairlineRo = new ResizeObserver(this.onHairlineSync);
28497
29671
  this.hairlineRo.observe(this.el);
28498
29672
  doc.defaultView?.addEventListener("resize", this.onHairlineSync);
28499
29673
  this.renderActions();
28500
- 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 }));
28501
29675
  this.tooltips.push(new Tooltip(this.styleButton, { content: "Chart style", triggerId: "vela-topbar-style", host }));
28502
29676
  if (this.layoutButton && opts.layout) {
28503
29677
  this.tooltips.push(new Tooltip(this.layoutButton, { content: "Layout", triggerId: "vela-topbar-layout", host }));
@@ -28514,11 +29688,12 @@ var Topbar = class {
28514
29688
  });
28515
29689
  }
28516
29690
  this.tfMenu = new Menu({
28517
- trigger: this.tfButton,
29691
+ trigger: this.tfCaret,
28518
29692
  triggerId: "vela-topbar-tf",
28519
29693
  host,
28520
29694
  items: this.tfItems(),
28521
29695
  onSelect: (id) => opts.onTimeframe(id),
29696
+ onFavorite: (id, on) => opts.onTimeframeFavorite?.(id, on),
28522
29697
  // Timeframe labels are two-or-three characters ("1m", "4h", "1D") — the
28523
29698
  // stylesheet's default min-width would leave the list mostly empty.
28524
29699
  minWidth: "84px"
@@ -28536,8 +29711,48 @@ var Topbar = class {
28536
29711
  }
28537
29712
  setTimeframe(tf) {
28538
29713
  this.timeframe = tf;
28539
- this.tfButton.textContent = timeframeLabel(tf);
28540
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();
29721
+ this.tfMenu.setItems(this.tfItems());
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();
28541
29756
  }
28542
29757
  setPriceStyle(style) {
28543
29758
  this.priceStyle = style;
@@ -28615,9 +29830,9 @@ var Topbar = class {
28615
29830
  }
28616
29831
  /** Reflect a docked side panel's open state on its button — the panels toggle each other,
28617
29832
  * so the dock pushes the state rather than the button assuming it. */
28618
- setPanelActive(id, open) {
29833
+ setPanelActive(id, open2) {
28619
29834
  const btn = this.panelBtns.get(id);
28620
- if (btn) btn.dataset.active = open ? "1" : "";
29835
+ if (btn) btn.dataset.active = open2 ? "1" : "";
28621
29836
  }
28622
29837
  destroy() {
28623
29838
  this.hairlineRo?.disconnect();
@@ -28655,7 +29870,13 @@ var Topbar = class {
28655
29870
  }
28656
29871
  }
28657
29872
  tfItems() {
28658
- 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
+ }));
28659
29880
  }
28660
29881
  styleItems() {
28661
29882
  return priceStyleIds().map((id, i) => ({
@@ -28678,10 +29899,10 @@ function fmtPrice(n, dp) {
28678
29899
  const d = dp ?? decimalsFor(n);
28679
29900
  return n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
28680
29901
  }
28681
- function fmtChange(open, close) {
28682
- if (open == null || close == null || !Number.isFinite(open) || !Number.isFinite(close) || open === 0) return "";
28683
- const diff = close - open;
28684
- const pct = diff / open * 100;
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;
28685
29906
  const sign = diff >= 0 ? "+" : "";
28686
29907
  return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
28687
29908
  }
@@ -28730,7 +29951,7 @@ function tickerIconEl(doc, base, name, className) {
28730
29951
  }
28731
29952
 
28732
29953
  // src/widget/statusline.ts
28733
- var STYLE_ID9 = "vela-widget-statusline";
29954
+ var STYLE_ID8 = "vela-widget-statusline";
28734
29955
  var CSS5 = `
28735
29956
  .vela-statusline {
28736
29957
  position: absolute;
@@ -28922,7 +30143,7 @@ var Statusline = class {
28922
30143
  this.fitMode = false;
28923
30144
  this.fitRO = null;
28924
30145
  const doc = host.ownerDocument;
28925
- injectStyles(STYLE_ID9, CSS5, doc);
30146
+ injectStyles(STYLE_ID8, CSS5, doc);
28926
30147
  host.classList.add("vela-has-statusline");
28927
30148
  this.el = doc.createElement("div");
28928
30149
  this.el.className = "vela-statusline";
@@ -29097,7 +30318,7 @@ var Statusline = class {
29097
30318
  var MAX_FONT_PX = 36;
29098
30319
  var MIN_FONT_PX = 12;
29099
30320
  var FILL = 0.9;
29100
- var STYLE_ID10 = "vela-widget-watermark";
30321
+ var STYLE_ID9 = "vela-widget-watermark";
29101
30322
  var CSS6 = `
29102
30323
  .vela-watermark {
29103
30324
  position: absolute;
@@ -29136,7 +30357,7 @@ var Watermark = class {
29136
30357
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
29137
30358
  * fires during chart construction, before any subscriber can see it. */
29138
30359
  this.loading = true;
29139
- injectStyles(STYLE_ID10, CSS6, host.ownerDocument);
30360
+ injectStyles(STYLE_ID9, CSS6, host.ownerDocument);
29140
30361
  this.el = host.ownerDocument.createElement("div");
29141
30362
  this.el.className = "vela-watermark";
29142
30363
  this.el.dataset.velaScreenshot = "under";
@@ -29200,7 +30421,7 @@ var RANGE_PRESETS = [
29200
30421
  { id: "ALL", tf: "W", preset: "ALL", bars: 5e3 }
29201
30422
  // everything the provider serves
29202
30423
  ];
29203
- var STYLE_ID11 = "vela-widget-bottombar";
30424
+ var STYLE_ID10 = "vela-widget-bottombar";
29204
30425
  var CSS7 = `
29205
30426
  .vela-widget-bottombar {
29206
30427
  display: flex;
@@ -29276,7 +30497,7 @@ var Bottombar = class {
29276
30497
  this.timer = null;
29277
30498
  this.timezone = opts.timezone;
29278
30499
  const doc = host.ownerDocument;
29279
- injectStyles(STYLE_ID11, CSS7, doc);
30500
+ injectStyles(STYLE_ID10, CSS7, doc);
29280
30501
  this.el = doc.createElement("div");
29281
30502
  this.el.className = "vela-widget-bottombar";
29282
30503
  for (const preset of RANGE_PRESETS) {
@@ -29414,7 +30635,7 @@ function filterSymbols(list, query, limit = 100) {
29414
30635
  }
29415
30636
  return [...prefix, ...substr, ...desc, ...venue].slice(0, limit);
29416
30637
  }
29417
- var STYLE_ID12 = "vela-widget-symbolpicker";
30638
+ var STYLE_ID11 = "vela-widget-symbolpicker";
29418
30639
  var CSS8 = `
29419
30640
  .vela-sp-searchrow {
29420
30641
  display: flex;
@@ -29512,7 +30733,7 @@ var SymbolPicker = class {
29512
30733
  this.seed = "";
29513
30734
  this.activeTab = "All";
29514
30735
  const doc = (opts.host ?? document.body).ownerDocument;
29515
- injectStyles(STYLE_ID12, CSS8, doc);
30736
+ injectStyles(STYLE_ID11, CSS8, doc);
29516
30737
  this.input = doc.createElement("input");
29517
30738
  this.input.className = "vela-sp-input";
29518
30739
  this.input.placeholder = "Search symbol\u2026";
@@ -29542,8 +30763,8 @@ var SymbolPicker = class {
29542
30763
  host: opts.host,
29543
30764
  closeOnInteractOutside: true,
29544
30765
  content: (body) => body.append(searchRow, this.tabs, this.list),
29545
- onOpenChange: (open) => {
29546
- if (open) {
30766
+ onOpenChange: (open2) => {
30767
+ if (open2) {
29547
30768
  this.input.value = this.seed;
29548
30769
  this.seed = "";
29549
30770
  this.refresh();
@@ -29552,7 +30773,7 @@ var SymbolPicker = class {
29552
30773
  this.input.setSelectionRange(this.input.value.length, this.input.value.length);
29553
30774
  }, 0);
29554
30775
  }
29555
- opts.onOpenChange?.(open);
30776
+ opts.onOpenChange?.(open2);
29556
30777
  }
29557
30778
  });
29558
30779
  this.input.addEventListener("input", () => this.refresh());
@@ -29834,7 +31055,7 @@ function treeIsEmpty(panes) {
29834
31055
  }
29835
31056
 
29836
31057
  // src/widget/side-panel.ts
29837
- var STYLE_ID13 = "vela-widget-sidepanel";
31058
+ var STYLE_ID12 = "vela-widget-sidepanel";
29838
31059
  var DEFAULT_PANEL_WIDTH = 280;
29839
31060
  var DEFAULT_PANEL_MIN_WIDTH = 200;
29840
31061
  var DEFAULT_PANEL_MAX_WIDTH = 640;
@@ -29938,7 +31159,7 @@ var SidePanel = class {
29938
31159
  * a programmatic {@link setWidth}, so restoring a persisted width raises no change. */
29939
31160
  this.onWidthChange = null;
29940
31161
  const doc = host.ownerDocument;
29941
- injectStyles(STYLE_ID13, CSS9, doc);
31162
+ injectStyles(STYLE_ID12, CSS9, doc);
29942
31163
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
29943
31164
  this.maxWidth = opts.resizable ? opts.maxWidth ?? DEFAULT_PANEL_MAX_WIDTH : Number.POSITIVE_INFINITY;
29944
31165
  this.declaredWidth = clampPanelWidth(opts.width ?? DEFAULT_PANEL_WIDTH, this.minWidth, this.maxWidth);
@@ -29970,10 +31191,10 @@ var SidePanel = class {
29970
31191
  return !this.el.hidden;
29971
31192
  }
29972
31193
  /** Open/close the panel — a bare call flips it. */
29973
- toggle(open = this.el.hidden) {
29974
- if (open === !this.el.hidden) return;
29975
- this.el.hidden = !open;
29976
- this.onOpenChange?.(open);
31194
+ toggle(open2 = this.el.hidden) {
31195
+ if (open2 === !this.el.hidden) return;
31196
+ this.el.hidden = !open2;
31197
+ this.onOpenChange?.(open2);
29977
31198
  }
29978
31199
  /** The scrolling body, for a panel filled from OUTSIDE the class — a contributed panel's
29979
31200
  * `mount` receives exactly this element. Subclasses use the protected `body`. */
@@ -30047,7 +31268,7 @@ var SidePanel = class {
30047
31268
  };
30048
31269
 
30049
31270
  // src/widget/object-tree.ts
30050
- var STYLE_ID14 = "vela-widget-objtree";
31271
+ var STYLE_ID13 = "vela-widget-objtree";
30051
31272
  var CSS10 = `
30052
31273
  /* One pane's block. The transparent border reserves the drop-target outline. */
30053
31274
  .vela-ot-pane {
@@ -30286,7 +31507,7 @@ var ObjectTree = class extends SidePanel {
30286
31507
  this.menu = null;
30287
31508
  this.menuActions = /* @__PURE__ */ new Map();
30288
31509
  this.drag = null;
30289
- injectStyles(STYLE_ID14, CSS10, host.ownerDocument);
31510
+ injectStyles(STYLE_ID13, CSS10, host.ownerDocument);
30290
31511
  this.body.addEventListener("contextmenu", (e) => this.onContextMenu(e));
30291
31512
  this.body.addEventListener("pointerdown", (e) => this.onPointerDown(e));
30292
31513
  }
@@ -30296,9 +31517,9 @@ var ObjectTree = class extends SidePanel {
30296
31517
  set groups(next) {
30297
31518
  if (this.chart) this.groupsPerChart.set(this.chart, next);
30298
31519
  }
30299
- toggle(open = this.el.hidden) {
30300
- super.toggle(open);
30301
- if (open) this.refresh();
31520
+ toggle(open2 = this.el.hidden) {
31521
+ super.toggle(open2);
31522
+ if (open2) this.refresh();
30302
31523
  }
30303
31524
  setSymbol(symbol) {
30304
31525
  this.symbolName = parseSymbol(symbol).ticker;
@@ -31268,7 +32489,7 @@ var ObjectTree = class extends SidePanel {
31268
32489
  };
31269
32490
 
31270
32491
  // src/widget/data-window.ts
31271
- var STYLE_ID15 = "vela-widget-datawindow";
32492
+ var STYLE_ID14 = "vela-widget-datawindow";
31272
32493
  var CSS11 = `
31273
32494
  .vela-dw-group {
31274
32495
  padding: 10px 8px 4px;
@@ -31328,11 +32549,11 @@ var DataWindow = class extends SidePanel {
31328
32549
  super(host, "Data window", "vela-dw");
31329
32550
  this.chart = null;
31330
32551
  this.unsubs = [];
31331
- injectStyles(STYLE_ID15, CSS11, host.ownerDocument);
32552
+ injectStyles(STYLE_ID14, CSS11, host.ownerDocument);
31332
32553
  }
31333
- toggle(open = this.el.hidden) {
31334
- super.toggle(open);
31335
- if (open) this.refresh();
32554
+ toggle(open2 = this.el.hidden) {
32555
+ super.toggle(open2);
32556
+ if (open2) this.refresh();
31336
32557
  }
31337
32558
  /** (Re)bind to a chart instance — called after every widget rebuild. */
31338
32559
  onChart(chart) {
@@ -31446,8 +32667,8 @@ var PanelDock = class {
31446
32667
  }
31447
32668
  }
31448
32669
  /** Open/close one panel by id — a bare call flips it. Unknown ids are ignored. */
31449
- toggle(id, open) {
31450
- this.entries.find((e) => e.id === id)?.panel.toggle(open);
32670
+ toggle(id, open2) {
32671
+ this.entries.find((e) => e.id === id)?.panel.toggle(open2);
31451
32672
  }
31452
32673
  /** The open panel's id, or null when the column is closed. */
31453
32674
  get openId() {
@@ -31461,8 +32682,8 @@ var PanelDock = class {
31461
32682
  /** The dock's persistable state, or null when there is nothing worth saving. */
31462
32683
  getState() {
31463
32684
  const out = {};
31464
- const open = this.openId;
31465
- if (open) out.open = open;
32685
+ const open2 = this.openId;
32686
+ if (open2) out.open = open2;
31466
32687
  if (this.widths.size > 0) out.widths = Object.fromEntries(this.widths);
31467
32688
  return out.open || out.widths ? out : null;
31468
32689
  }
@@ -31495,13 +32716,13 @@ var PanelDock = class {
31495
32716
  this.entries.sort((a, b) => a.order - b.order);
31496
32717
  const stored = this.widths.get(entry.id);
31497
32718
  if (stored !== void 0) entry.panel.setWidth(stored);
31498
- entry.panel.onOpenChange = (open) => {
31499
- if (open) {
32719
+ entry.panel.onOpenChange = (open2) => {
32720
+ if (open2) {
31500
32721
  for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
31501
32722
  this.pendingOpen = null;
31502
32723
  }
31503
- this.deps.chrome.setPanelActive(entry.id, open);
31504
- if (open) entry.handle?.onOpen?.();
32724
+ this.deps.chrome.setPanelActive(entry.id, open2);
32725
+ if (open2) entry.handle?.onOpen?.();
31505
32726
  this.deps.changed?.();
31506
32727
  };
31507
32728
  entry.panel.onWidthChange = (px) => {
@@ -31530,7 +32751,7 @@ var PanelDock = class {
31530
32751
  };
31531
32752
 
31532
32753
  // src/widget/shortcuts-help.ts
31533
- var STYLE_ID16 = "vela-widget-shortcuts";
32754
+ var STYLE_ID15 = "vela-widget-shortcuts";
31534
32755
  var CSS12 = `
31535
32756
  .vela-sh-cat {
31536
32757
  color: var(--vela-fg-muted);
@@ -31556,16 +32777,16 @@ var ShortcutsHelp = class {
31556
32777
  constructor(keymap, host, onOpenChange) {
31557
32778
  this.keymap = keymap;
31558
32779
  const doc = (host ?? document.body).ownerDocument;
31559
- injectStyles(STYLE_ID16, CSS12, doc);
32780
+ injectStyles(STYLE_ID15, CSS12, doc);
31560
32781
  this.list = doc.createElement("div");
31561
32782
  this.dialog = new Dialog({
31562
32783
  title: "Keyboard shortcuts",
31563
32784
  host,
31564
32785
  closeOnInteractOutside: true,
31565
32786
  content: (body) => body.appendChild(this.list),
31566
- onOpenChange: (open) => {
31567
- if (open) this.refresh();
31568
- onOpenChange?.(open);
32787
+ onOpenChange: (open2) => {
32788
+ if (open2) this.refresh();
32789
+ onOpenChange?.(open2);
31569
32790
  }
31570
32791
  });
31571
32792
  }
@@ -31844,7 +33065,7 @@ var ChartContextMenu = class {
31844
33065
  };
31845
33066
 
31846
33067
  // src/widget/indicator-picker.ts
31847
- var STYLE_ID17 = "vela-widget-indpicker";
33068
+ var STYLE_ID16 = "vela-widget-indpicker";
31848
33069
  var CSS13 = `
31849
33070
  .vela-ip-searchrow {
31850
33071
  display: flex;
@@ -31922,7 +33143,7 @@ var IndicatorPicker = class {
31922
33143
  this.isOpen = false;
31923
33144
  this.opts = opts;
31924
33145
  const doc = (opts.host ?? document.body).ownerDocument;
31925
- injectStyles(STYLE_ID17, CSS13, doc);
33146
+ injectStyles(STYLE_ID16, CSS13, doc);
31926
33147
  this.search = doc.createElement("input");
31927
33148
  this.search.className = "vela-ip-search";
31928
33149
  this.search.placeholder = "Search\u2026";
@@ -31940,14 +33161,14 @@ var IndicatorPicker = class {
31940
33161
  closeOnInteractOutside: true,
31941
33162
  // The picker adds/removes indicators live — keep the chart undimmed behind it.
31942
33163
  content: (body) => body.append(searchRow, this.list),
31943
- onOpenChange: (open) => {
31944
- this.isOpen = open;
31945
- if (open) {
33164
+ onOpenChange: (open2) => {
33165
+ this.isOpen = open2;
33166
+ if (open2) {
31946
33167
  this.search.value = "";
31947
33168
  this.refresh();
31948
33169
  if (!this.search.closest('[data-layout="mobile"]')) setTimeout(() => this.search.focus(), 0);
31949
33170
  }
31950
- opts.onOpenChange?.(open);
33171
+ opts.onOpenChange?.(open2);
31951
33172
  }
31952
33173
  });
31953
33174
  this.list.addEventListener("click", (e) => {
@@ -32050,7 +33271,7 @@ var IndicatorPicker = class {
32050
33271
  };
32051
33272
 
32052
33273
  // src/widget/timeframe-quick.ts
32053
- var STYLE_ID18 = "vela-widget-tfquick";
33274
+ var STYLE_ID17 = "vela-widget-tfquick";
32054
33275
  var CSS14 = `
32055
33276
  .vela-tq-input {
32056
33277
  display: block;
@@ -32074,7 +33295,7 @@ var CSS14 = `
32074
33295
  var TimeframeQuick = class {
32075
33296
  constructor(opts) {
32076
33297
  const doc = (opts.host ?? document.body).ownerDocument;
32077
- injectStyles(STYLE_ID18, CSS14, doc);
33298
+ injectStyles(STYLE_ID17, CSS14, doc);
32078
33299
  this.input = doc.createElement("input");
32079
33300
  this.input.className = "vela-tq-input";
32080
33301
  this.input.setAttribute("spellcheck", "false");
@@ -32086,7 +33307,7 @@ var TimeframeQuick = class {
32086
33307
  draggable: true,
32087
33308
  closeOnInteractOutside: true,
32088
33309
  content: (body) => body.append(this.input, this.hint),
32089
- onOpenChange: (open) => opts.onOpenChange?.(open)
33310
+ onOpenChange: (open2) => opts.onOpenChange?.(open2)
32090
33311
  });
32091
33312
  this.input.addEventListener("input", () => this.renderHint());
32092
33313
  this.input.addEventListener("keydown", (e) => {
@@ -32256,7 +33477,7 @@ var LayoutModeController = class {
32256
33477
  };
32257
33478
 
32258
33479
  // src/widget/mobile-bar.ts
32259
- var STYLE_ID19 = "vela-widget-mobilebar";
33480
+ var STYLE_ID18 = "vela-widget-mobilebar";
32260
33481
  var CSS15 = `
32261
33482
  .vela-mobilebar {
32262
33483
  display: none;
@@ -32307,7 +33528,7 @@ var MobileBar = class {
32307
33528
  constructor(host, opts) {
32308
33529
  this.opts = opts;
32309
33530
  const doc = host.ownerDocument;
32310
- injectStyles(STYLE_ID19, CSS15, doc);
33531
+ injectStyles(STYLE_ID18, CSS15, doc);
32311
33532
  this.el = doc.createElement("div");
32312
33533
  this.el.className = "vela-mobilebar";
32313
33534
  const item = (cls, label, onClick, icon2) => {
@@ -32366,7 +33587,7 @@ var MobileBar = class {
32366
33587
  };
32367
33588
 
32368
33589
  // src/widget/timeframe-drawer.ts
32369
- var STYLE_ID20 = "vela-widget-tf-drawer";
33590
+ var STYLE_ID19 = "vela-widget-tf-drawer";
32370
33591
  var CSS16 = `
32371
33592
  .vela-tfd-heading {
32372
33593
  padding: 4px 2px 8px;
@@ -32423,7 +33644,7 @@ var TimeframeDrawer = class {
32423
33644
  this.rangeChips = /* @__PURE__ */ new Map();
32424
33645
  this.tfChips = /* @__PURE__ */ new Map();
32425
33646
  const doc = opts.host.ownerDocument;
32426
- injectStyles(STYLE_ID20, CSS16, doc);
33647
+ injectStyles(STYLE_ID19, CSS16, doc);
32427
33648
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
32428
33649
  const rangeHeading = doc.createElement("div");
32429
33650
  rangeHeading.className = "vela-tfd-heading";
@@ -32481,7 +33702,7 @@ var TimeframeDrawer = class {
32481
33702
  };
32482
33703
 
32483
33704
  // src/widget/drawings-drawer.ts
32484
- var STYLE_ID21 = "vela-widget-drawings-drawer";
33705
+ var STYLE_ID20 = "vela-widget-drawings-drawer";
32485
33706
  var CSS17 = `
32486
33707
  /* Search + tabs stay pinned while the tool list scrolls underneath. */
32487
33708
  .vela-dd-sticky {
@@ -32584,7 +33805,7 @@ var DrawingsDrawer = class {
32584
33805
  this.definition = { groups: [] };
32585
33806
  this.activeGroup = "";
32586
33807
  const doc = opts.host.ownerDocument;
32587
- injectStyles(STYLE_ID21, CSS17, doc);
33808
+ injectStyles(STYLE_ID20, CSS17, doc);
32588
33809
  this.drawer = new Drawer({
32589
33810
  host: opts.host,
32590
33811
  title: "Drawings",
@@ -32726,7 +33947,7 @@ var DrawingsDrawer = class {
32726
33947
  };
32727
33948
 
32728
33949
  // src/widget/more-drawer.ts
32729
- var STYLE_ID22 = "vela-widget-more-drawer";
33950
+ var STYLE_ID21 = "vela-widget-more-drawer";
32730
33951
  var CSS18 = `
32731
33952
  .vela-md-actions {
32732
33953
  display: flex;
@@ -32802,7 +34023,7 @@ var MoreDrawer = class {
32802
34023
  constructor(opts) {
32803
34024
  this.opts = opts;
32804
34025
  this.view = "main";
32805
- injectStyles(STYLE_ID22, CSS18, opts.host.ownerDocument);
34026
+ injectStyles(STYLE_ID21, CSS18, opts.host.ownerDocument);
32806
34027
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
32807
34028
  }
32808
34029
  open() {
@@ -32994,7 +34215,7 @@ var MoreDrawer = class {
32994
34215
  };
32995
34216
 
32996
34217
  // src/widget/timezone-drawer.ts
32997
- var STYLE_ID23 = "vela-widget-timezone-drawer";
34218
+ var STYLE_ID22 = "vela-widget-timezone-drawer";
32998
34219
  var CSS19 = `
32999
34220
  .vela-tzd-list { padding: 2px 0 4px; }
33000
34221
  .vela-tzd-row {
@@ -33014,7 +34235,7 @@ var CSS19 = `
33014
34235
  var TimezoneDrawer = class {
33015
34236
  constructor(opts) {
33016
34237
  this.opts = opts;
33017
- injectStyles(STYLE_ID23, CSS19, opts.host.ownerDocument);
34238
+ injectStyles(STYLE_ID22, CSS19, opts.host.ownerDocument);
33018
34239
  this.drawer = new Drawer({ host: opts.host, title: "Time zone", onOpenChange: opts.onOpenChange });
33019
34240
  }
33020
34241
  open() {
@@ -33052,7 +34273,7 @@ var TimezoneDrawer = class {
33052
34273
  };
33053
34274
 
33054
34275
  // src/widget/price-scale-drawer.ts
33055
- var STYLE_ID24 = "vela-widget-price-scale-drawer";
34276
+ var STYLE_ID23 = "vela-widget-price-scale-drawer";
33056
34277
  var CSS20 = `
33057
34278
  .vela-psd-list { padding: 2px 0 4px; }
33058
34279
  .vela-psd-row {
@@ -33082,7 +34303,7 @@ var PriceScaleDrawer = class {
33082
34303
  constructor(opts) {
33083
34304
  this.opts = opts;
33084
34305
  this.pane = null;
33085
- injectStyles(STYLE_ID24, CSS20, opts.host.ownerDocument);
34306
+ injectStyles(STYLE_ID23, CSS20, opts.host.ownerDocument);
33086
34307
  this.drawer = new Drawer({ host: opts.host, title: "Price scale", onOpenChange: opts.onOpenChange });
33087
34308
  }
33088
34309
  open() {
@@ -33218,7 +34439,7 @@ var PriceScaleDrawer = class {
33218
34439
  };
33219
34440
 
33220
34441
  // src/widget/drawing-pill.ts
33221
- var STYLE_ID25 = "vela-widget-drawing-pill";
34442
+ var STYLE_ID24 = "vela-widget-drawing-pill";
33222
34443
  var CSS21 = `
33223
34444
  .vela-drawpill { display: none; }
33224
34445
  [data-layout='mobile'] .vela-drawpill {
@@ -33279,7 +34500,7 @@ var DrawingPill = class {
33279
34500
  this.chart = null;
33280
34501
  this.chartSubs = [];
33281
34502
  const doc = host.ownerDocument;
33282
- injectStyles(STYLE_ID25, CSS21, doc);
34503
+ injectStyles(STYLE_ID24, CSS21, doc);
33283
34504
  this.el = doc.createElement("div");
33284
34505
  this.el.className = "vela-drawpill";
33285
34506
  this.el.hidden = true;
@@ -33388,6 +34609,10 @@ function sanitizeState(doc) {
33388
34609
  const favs = d.favorites.filter((f) => typeof f === "string");
33389
34610
  if (favs.length > 0) out.favorites = favs;
33390
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
+ }
33391
34616
  const sync = sanitizeSync(d.sync);
33392
34617
  if (sync) out.sync = sync;
33393
34618
  const tracks = sanitizeTrackSizes(d.trackSizes);
@@ -33661,7 +34886,7 @@ var WidgetHistory = class {
33661
34886
  };
33662
34887
 
33663
34888
  // src/widget/toast.ts
33664
- var STYLE_ID26 = "vela-widget-toast";
34889
+ var STYLE_ID25 = "vela-widget-toast";
33665
34890
  var CSS22 = `
33666
34891
  .vela-toast {
33667
34892
  position: absolute;
@@ -33692,7 +34917,7 @@ var CSS22 = `
33692
34917
  var Toast = class {
33693
34918
  constructor(host) {
33694
34919
  this.timer = null;
33695
- injectStyles(STYLE_ID26, CSS22, host.ownerDocument);
34920
+ injectStyles(STYLE_ID25, CSS22, host.ownerDocument);
33696
34921
  this.el = host.ownerDocument.createElement("div");
33697
34922
  this.el.className = "vela-toast";
33698
34923
  host.appendChild(this.el);
@@ -33828,6 +35053,8 @@ var VelaWidget = class {
33828
35053
  this.stateListeners = /* @__PURE__ */ new Set();
33829
35054
  /** Favorite drawing-tool types — mirrored from chart events, reapplied on rebuilds. */
33830
35055
  this.favs = [];
35056
+ /** Favorite timeframes — the topbar's quick-switch chips, persisted with the shell. */
35057
+ this.tfFavs = [];
33831
35058
  /** Mounted attachment disposers (by id), torn down at destroy. */
33832
35059
  this.attachmentDisposers = /* @__PURE__ */ new Map();
33833
35060
  this.onUnload = () => this.persistNow();
@@ -33870,6 +35097,7 @@ var VelaWidget = class {
33870
35097
  this.indicatorTitlesOn = bootCell?.indicatorTitles ?? true;
33871
35098
  this.indicatorValuesOn = bootCell?.indicatorValues ?? true;
33872
35099
  this.favs = boot?.favorites ? [...boot.favorites] : [];
35100
+ this.tfFavs = boot?.timeframeFavorites ? [...boot.timeframeFavorites] : [];
33873
35101
  this.savedConfig = bootCell?.rendererConfig ?? null;
33874
35102
  this.savedDrawings = bootCell?.drawings ?? null;
33875
35103
  this.pendingIndicators = bootCell?.indicators ?? null;
@@ -33883,9 +35111,9 @@ var VelaWidget = class {
33883
35111
  this.symbolPicker = new SymbolPicker({
33884
35112
  host: this.root,
33885
35113
  onSelect: (symbol) => this.setSymbol(symbol),
33886
- onOpenChange: (open) => {
33887
- if (open) this.inner?.renderer.closeDialogs();
33888
- this.trackDialog(open);
35114
+ onOpenChange: (open2) => {
35115
+ if (open2) this.inner?.renderer.closeDialogs();
35116
+ this.trackDialog(open2);
33889
35117
  }
33890
35118
  });
33891
35119
  this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
@@ -33908,15 +35136,15 @@ var VelaWidget = class {
33908
35136
  if (i < present.length) this.removeNative(present[i].type);
33909
35137
  else this.removeInstance(i - present.length);
33910
35138
  },
33911
- onOpenChange: (open) => {
33912
- if (open) this.inner?.renderer.closeDialogs();
33913
- this.trackDialog(open);
35139
+ onOpenChange: (open2) => {
35140
+ if (open2) this.inner?.renderer.closeDialogs();
35141
+ this.trackDialog(open2);
33914
35142
  }
33915
35143
  }) : null;
33916
35144
  this.tfQuick = new TimeframeQuick({
33917
35145
  host: this.root,
33918
35146
  onApply: (tf) => this.setTimeframe(tf),
33919
- onOpenChange: (open) => this.trackDialog(open)
35147
+ onOpenChange: (open2) => this.trackDialog(open2)
33920
35148
  });
33921
35149
  const picker = this.indicatorPicker;
33922
35150
  this.topbar = new Topbar(this.root, {
@@ -33929,8 +35157,10 @@ var VelaWidget = class {
33929
35157
  onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
33930
35158
  timeframe: this.timeframe,
33931
35159
  timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
35160
+ timeframeFavorites: this.tfFavs,
33932
35161
  priceStyle: this.priceStyle,
33933
35162
  onTimeframe: (tf) => this.setTimeframe(tf),
35163
+ onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
33934
35164
  onPriceStyle: (style) => this.setPriceStyle(style),
33935
35165
  getContext: () => this.context()
33936
35166
  });
@@ -34041,7 +35271,7 @@ var VelaWidget = class {
34041
35271
  label: "Show this shortcuts panel",
34042
35272
  category: "Help",
34043
35273
  run: () => {
34044
- 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)));
34045
35275
  this.shortcutsHelp.open();
34046
35276
  }
34047
35277
  });
@@ -34075,7 +35305,7 @@ var VelaWidget = class {
34075
35305
  activeRange: () => this.activeRangeId,
34076
35306
  onTimeframe: (tf) => this.setTimeframe(tf),
34077
35307
  onRange: (preset) => this.applyRange(preset),
34078
- onOpenChange: (open) => this.trackDialog(open)
35308
+ onOpenChange: (open2) => this.trackDialog(open2)
34079
35309
  }));
34080
35310
  this.tfDrawer.open();
34081
35311
  }
@@ -34087,7 +35317,7 @@ var VelaWidget = class {
34087
35317
  isFavorite: (type) => this.inner?.drawings.isFavorite(type) ?? false,
34088
35318
  onFavorite: (type, on) => this.inner?.drawings.setFavorite(type, on),
34089
35319
  onSelect: (type) => this.inner?.drawings.setTool(type),
34090
- onOpenChange: (open) => this.trackDialog(open)
35320
+ onOpenChange: (open2) => this.trackDialog(open2)
34091
35321
  }));
34092
35322
  this.drawingsDrawer.open();
34093
35323
  }
@@ -34108,7 +35338,7 @@ var VelaWidget = class {
34108
35338
  // Left-aligned actions have their own bottom-bar stop — only the rest
34109
35339
  // lands in the drawer, or every left action would appear twice.
34110
35340
  actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
34111
- onOpenChange: (open) => this.trackDialog(open)
35341
+ onOpenChange: (open2) => this.trackDialog(open2)
34112
35342
  }));
34113
35343
  this.moreDrawer.open();
34114
35344
  }
@@ -34117,7 +35347,7 @@ var VelaWidget = class {
34117
35347
  host: this.root,
34118
35348
  timezone: () => this.timezone,
34119
35349
  onTimezone: (zone) => this.setTimezone(zone),
34120
- onOpenChange: (open) => this.trackDialog(open)
35350
+ onOpenChange: (open2) => this.trackDialog(open2)
34121
35351
  }));
34122
35352
  this.timezoneDrawer.open();
34123
35353
  }
@@ -34127,7 +35357,7 @@ var VelaWidget = class {
34127
35357
  host: this.root,
34128
35358
  chart: () => this.inner,
34129
35359
  pressY: () => this.priceScalePressY,
34130
- onOpenChange: (open) => this.trackDialog(open)
35360
+ onOpenChange: (open2) => this.trackDialog(open2)
34131
35361
  }));
34132
35362
  this.priceScaleDrawer.open();
34133
35363
  }
@@ -34156,7 +35386,7 @@ var VelaWidget = class {
34156
35386
  setTimeframe: (tf) => this.setTimeframe(tf),
34157
35387
  setPriceStyle: (style) => this.setPriceStyle(style),
34158
35388
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
34159
- togglePanel: (id, open) => this.dock.toggle(id, open),
35389
+ togglePanel: (id, open2) => this.dock.toggle(id, open2),
34160
35390
  host: this.root,
34161
35391
  toast: (message, kind) => this.toast?.show(message, kind)
34162
35392
  };
@@ -34215,6 +35445,14 @@ var VelaWidget = class {
34215
35445
  this.markStateDirty();
34216
35446
  void this.inner?.setMarket({ timeframe: tf, bars: this.bars });
34217
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
+ }
34218
35456
  /**
34219
35457
  * Switch the chart symbol in place. An `EXCHANGE:` prefix pins the venue (the
34220
35458
  * symbol picker composes one from the row the user pointed at) — a bare ticker
@@ -34272,6 +35510,7 @@ var VelaWidget = class {
34272
35510
  });
34273
35511
  const state = { version: 1, layout: "1", activeCellId: "c1", timezone: this.timezone, charts: [{ id: "c1", ...cell }] };
34274
35512
  if (this.favs.length > 0) state.favorites = [...this.favs];
35513
+ if (this.tfFavs.length > 0) state.timeframeFavorites = [...this.tfFavs];
34275
35514
  const panels2 = this.dock.getState();
34276
35515
  if (panels2) state.panels = panels2;
34277
35516
  return state;
@@ -34295,6 +35534,10 @@ var VelaWidget = class {
34295
35534
  this.favs = [...st.favorites];
34296
35535
  this.inner?.drawings.setFavorites(this.favs);
34297
35536
  }
35537
+ if (st.timeframeFavorites) {
35538
+ this.tfFavs = [...st.timeframeFavorites];
35539
+ this.topbar.setTimeframeFavorites(this.tfFavs);
35540
+ }
34298
35541
  this.dock.applyState(st.panels);
34299
35542
  if (cell) {
34300
35543
  const style = fromUrl.priceStyle ?? cell.priceStyle;
@@ -34869,9 +36112,9 @@ var VelaWidget = class {
34869
36112
  this.tfQuick.open(key);
34870
36113
  }
34871
36114
  }
34872
- trackDialog(open) {
34873
- this.openDialogs = Math.max(0, this.openDialogs + (open ? 1 : -1));
34874
- 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");
34875
36118
  else this.keymap.popScope("dialog");
34876
36119
  }
34877
36120
  downloadScreenshot() {