@luxalgo/vela 0.6.16 → 0.6.18

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 (46) hide show
  1. package/README.md +4 -0
  2. package/dist/{DataProvider-XLdJB86x.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
  3. package/dist/{DataProvider-xH2VBcFT.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
  4. package/dist/{chunk-GIJYO4LO.js → chunk-22QJKGXX.js} +666 -189
  5. package/dist/{chunk-SEOO6YG7.js → chunk-BXF7VNNE.js} +181 -111
  6. package/dist/{chunk-6DPAYEGO.js → chunk-CAFCLMPF.js} +1 -0
  7. package/dist/{chunk-I77F2SVE.js → chunk-IFJJPXSV.js} +1 -1
  8. package/dist/{chunk-MGU3USTP.js → chunk-T5Z5YUCF.js} +1 -1
  9. package/dist/{chunk-Q3TFJNJW.js → chunk-XCBJU674.js} +2 -2
  10. package/dist/{contributions-BEkxwA5F.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
  11. package/dist/{contributions-CoAX7_SF.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
  12. package/dist/index.cjs +662 -185
  13. package/dist/index.d.cts +15 -6
  14. package/dist/index.d.ts +15 -6
  15. package/dist/index.js +4 -4
  16. package/dist/{options-bdSAtp7y.d.ts → options-BCRmYALw.d.cts} +24 -5
  17. package/dist/{options-bdSAtp7y.d.cts → options-BCRmYALw.d.ts} +24 -5
  18. package/dist/{plugin-CNUgbRNq.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
  19. package/dist/{plugin-Dxyq35Zf.d.cts → plugin-DVUaROPo.d.cts} +3 -3
  20. package/dist/plugin.cjs +1 -0
  21. package/dist/plugin.d.cts +4 -4
  22. package/dist/plugin.d.ts +4 -4
  23. package/dist/plugin.js +2 -2
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/{statusline-model-CbPza-Wc.d.cts → statusline-model-7ECSDtgt.d.cts} +3 -3
  31. package/dist/{statusline-model-D7cWkw8A.d.ts → statusline-model-D1Fhs8Dk.d.ts} +3 -3
  32. package/dist/ui.cjs +1 -0
  33. package/dist/ui.d.cts +1 -1
  34. package/dist/ui.d.ts +1 -1
  35. package/dist/ui.js +3 -3
  36. package/dist/vela.global.js +662 -185
  37. package/dist/vela.global.min.js +52 -53
  38. package/dist/widget.cjs +839 -291
  39. package/dist/widget.d.cts +39 -12
  40. package/dist/widget.d.ts +39 -12
  41. package/dist/widget.js +7 -7
  42. package/dist/workspace.cjs +839 -291
  43. package/dist/workspace.d.cts +5 -5
  44. package/dist/workspace.d.ts +5 -5
  45. package/dist/workspace.js +6 -6
  46. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-I77F2SVE.js';
2
- import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-MGU3USTP.js';
3
- import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BEARISH, BULLISH, isDarkColor, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, INFO, svg24, svg24Solid } from './chunk-6DPAYEGO.js';
1
+ import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-IFJJPXSV.js';
2
+ import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-T5Z5YUCF.js';
3
+ import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BULLISH, CATEGORICAL, SERIES_LINE, BEARISH, isDarkColor, INFO, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
4
4
 
5
5
  // src/core/events/EventBus.ts
6
6
  var TypedEventBus = class {
@@ -2161,15 +2161,18 @@ var DrawingController = class {
2161
2161
  next = [...ids];
2162
2162
  }
2163
2163
  this.selectedIds = next.filter((id) => this.store.has(id));
2164
+ this.announceSelection();
2165
+ }
2166
+ /** Push the selection to the renderer and announce it (primary + the full list). */
2167
+ announceSelection() {
2164
2168
  this.port?.setSelection(this.selectedIds);
2165
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
2169
+ this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
2166
2170
  }
2167
2171
  /** Restore a history snapshot, reconciling selection against what survived. */
2168
2172
  restoreSnapshot(doc) {
2169
2173
  this.store.load(doc);
2170
2174
  this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
2171
- this.port?.setSelection(this.selectedIds);
2172
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
2175
+ this.announceSelection();
2173
2176
  }
2174
2177
  /** Delete drawings as one undo step; prune them from the selection. */
2175
2178
  deleteMany(ids) {
@@ -2302,6 +2305,9 @@ var DrawingController = class {
2302
2305
  case "duplicate":
2303
2306
  this.duplicate(i.ids);
2304
2307
  break;
2308
+ case "clone":
2309
+ if (this.enabled && i.docs.length) this.insertClones(i.docs);
2310
+ break;
2305
2311
  case "copy":
2306
2312
  this.copy(i.ids);
2307
2313
  break;
@@ -2368,6 +2374,23 @@ function attachChromeTooltip(anchor, opts) {
2368
2374
  clear();
2369
2375
  };
2370
2376
  }
2377
+ function chromeHint(text, opts) {
2378
+ const px = opts.size ?? 16;
2379
+ const el = document.createElement("span");
2380
+ el.className = "vela-hint";
2381
+ el.setAttribute("role", "img");
2382
+ el.setAttribute("aria-label", text);
2383
+ el.innerHTML = iconAt("info", px);
2384
+ el.style.cssText = `flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:${px}px;height:${px}px;border-radius:50%;cursor:help;user-select:none;background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;`;
2385
+ const hover = (on) => {
2386
+ el.style.background = on ? "var(--vela-active)" : "var(--vela-hover)";
2387
+ el.style.color = on ? "var(--vela-fg-bright)" : "var(--vela-fg-muted)";
2388
+ };
2389
+ el.addEventListener("pointerenter", () => hover(true));
2390
+ el.addEventListener("pointerleave", () => hover(false));
2391
+ const dispose = attachChromeTooltip(el, { host: opts.host, theme: opts.theme, text: () => text, wrap: true, delayMs: 250 });
2392
+ return { el, dispose };
2393
+ }
2371
2394
 
2372
2395
  // src/renderers/native/drawings/DrawingToolbar.ts
2373
2396
  var TOOLBAR_WIDTH = 44;
@@ -3001,6 +3024,24 @@ function arrowHitHtml(svg) {
3001
3024
  return `<span class="vela-dtb-hit vela-dtb-hit--arrow">${iconSpan(svg, 11)}</span>`;
3002
3025
  }
3003
3026
 
3027
+ // src/core/options.ts
3028
+ var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
3029
+ var LIVE_BAR_EASE_DEFAULT_MS = 90;
3030
+ var LIVE_BAR_EASE_MAX_MS = 1e3;
3031
+ function resolveLiveBarEaseMs(value) {
3032
+ if (value === true) return LIVE_BAR_EASE_DEFAULT_MS;
3033
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
3034
+ return Math.min(value, LIVE_BAR_EASE_MAX_MS);
3035
+ }
3036
+ function resolveAnimations(animations) {
3037
+ if (typeof animations === "boolean") return { animZoom: animations, animPan: animations, animLiveBar: 0 };
3038
+ return {
3039
+ animZoom: animations?.zoom ?? true,
3040
+ animPan: animations?.pan ?? true,
3041
+ animLiveBar: resolveLiveBarEaseMs(animations?.liveBar)
3042
+ };
3043
+ }
3044
+
3004
3045
  // src/core/model/series.ts
3005
3046
  function isLineLikeSeries(spec) {
3006
3047
  return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
@@ -3373,14 +3414,11 @@ var IndicatorInputsDialog = class {
3373
3414
  buildTextField(id, current, emit) {
3374
3415
  return buildFieldControl({ kind: "text", id, value: current, onChange: emit }).el;
3375
3416
  }
3376
- /** A hoverable ⓘ affordance carrying an input's `tooltip` (themed chrome tip; wraps). */
3417
+ /** The shared ⓘ hint carrying an input's `tooltip` (see {@link chromeHint}). */
3377
3418
  infoButton(tooltip) {
3378
- const b = document.createElement("span");
3379
- b.textContent = "i";
3380
- this.dialogTips.push(attachChromeTooltip(b, { host: this.host.container, theme: () => this.host.theme(), text: () => tooltip, wrap: true, delayMs: 250 }));
3381
- b.className = "vela-ind-hint";
3382
- b.style.cssText = "flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-weight:600;font-size:11px;font-family:inherit;line-height:1;cursor:help;user-select:none;";
3383
- return b;
3419
+ const { el, dispose } = chromeHint(tooltip, { host: this.host.container, theme: () => this.host.theme(), size: 18 });
3420
+ this.dialogTips.push(dispose);
3421
+ return el;
3384
3422
  }
3385
3423
  /** A dropdown over plain string options (value === label) — a thin case of {@link selectPairs}. */
3386
3424
  select(id, options, current, emit) {
@@ -3817,8 +3855,6 @@ function ensureDialogStyles() {
3817
3855
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
3818
3856
  .vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
3819
3857
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
3820
- .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;}
3821
- .vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
3822
3858
  .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;}
3823
3859
  .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;}
3824
3860
  .vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
@@ -5209,15 +5245,15 @@ function candleOverrideFor(style, bags) {
5209
5245
  if (!hasOwnCandlePaint(style)) return null;
5210
5246
  const bag = bags[style] ?? {};
5211
5247
  const color = (v) => isColor(v) ? v : null;
5212
- const bool = (v) => isBool(v) ? v : null;
5248
+ const bool2 = (v) => isBool(v) ? v : null;
5213
5249
  return {
5214
5250
  upColor: color(bag.candleUpColor),
5215
5251
  downColor: color(bag.candleDownColor),
5216
- bodyVisible: bool(bag.candleBodyVisible),
5217
- borderVisible: bool(bag.candleBorderVisible),
5252
+ bodyVisible: bool2(bag.candleBodyVisible),
5253
+ borderVisible: bool2(bag.candleBorderVisible),
5218
5254
  borderUpColor: color(bag.candleBorderUpColor),
5219
5255
  borderDownColor: color(bag.candleBorderDownColor),
5220
- wickVisible: bool(bag.candleWickVisible),
5256
+ wickVisible: bool2(bag.candleWickVisible),
5221
5257
  wickUpColor: color(bag.candleWickUpColor),
5222
5258
  wickDownColor: color(bag.candleWickDownColor)
5223
5259
  };
@@ -5362,7 +5398,8 @@ function mergeConfig(base, patch) {
5362
5398
  labelsVisible: isBool(ps.labelsVisible) ? ps.labelsVisible : base.priceScale.labelsVisible,
5363
5399
  currentPriceLine: isBool(ps.currentPriceLine) ? ps.currentPriceLine : base.priceScale.currentPriceLine,
5364
5400
  priceLabel: isBool(ps.priceLabel) ? ps.priceLabel : base.priceScale.priceLabel,
5365
- countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown
5401
+ countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
5402
+ animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
5366
5403
  },
5367
5404
  panes: {
5368
5405
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
@@ -7007,7 +7044,7 @@ var InputController = class {
7007
7044
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
7008
7045
  if (Number.isNaN(this.cursorX)) return;
7009
7046
  if (this.dragging && this.region !== "drawing") return;
7010
- this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
7047
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
7011
7048
  };
7012
7049
  this.onDown = (e) => {
7013
7050
  if (e.button === 1) {
@@ -7039,7 +7076,7 @@ var InputController = class {
7039
7076
  this.startY = y;
7040
7077
  if (this.deps.drawingsClaim?.(x, y)) {
7041
7078
  this.region = "drawing";
7042
- this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
7079
+ this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
7043
7080
  this.capture(e.pointerId);
7044
7081
  return;
7045
7082
  }
@@ -7048,6 +7085,11 @@ var InputController = class {
7048
7085
  this.capture(e.pointerId);
7049
7086
  return;
7050
7087
  }
7088
+ if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
7089
+ this.region = "drawing";
7090
+ this.capture(e.pointerId);
7091
+ return;
7092
+ }
7051
7093
  const vp = this.deps.getCoords().getViewport();
7052
7094
  this.deps.apply(vp);
7053
7095
  this.startRightOffset = vp.rightOffset;
@@ -7096,7 +7138,7 @@ var InputController = class {
7096
7138
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
7097
7139
  } else if (this.region === "drawing") {
7098
7140
  if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
7099
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
7141
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
7100
7142
  } else {
7101
7143
  const coords = this.deps.getCoords();
7102
7144
  const vp = coords.getViewport();
@@ -7119,7 +7161,7 @@ var InputController = class {
7119
7161
  const drawCursor = this.deps.drawingsCursor?.(x, y);
7120
7162
  const r = this.regionAt(x, y);
7121
7163
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
7122
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
7164
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
7123
7165
  }
7124
7166
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
7125
7167
  };
@@ -7460,12 +7502,12 @@ function defaultTradeMarkersState() {
7460
7502
  function mergeTradeMarkersState(base, patch) {
7461
7503
  const p = patch && typeof patch === "object" ? patch : {};
7462
7504
  const c = p.colors && typeof p.colors === "object" ? p.colors : {};
7463
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
7505
+ const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
7464
7506
  const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
7465
7507
  return {
7466
- visible: bool(p.visible, base.visible),
7467
- labels: bool(p.labels, base.labels),
7468
- qty: bool(p.qty, base.qty),
7508
+ visible: bool2(p.visible, base.visible),
7509
+ labels: bool2(p.labels, base.labels),
7510
+ qty: bool2(p.qty, base.qty),
7469
7511
  colors: {
7470
7512
  long: color(c.long, base.colors.long),
7471
7513
  short: color(c.short, base.colors.short),
@@ -10149,6 +10191,8 @@ var BUILTIN_SETTINGS_IDS = [
10149
10191
  "symbol.style.baseline.fill-bottom",
10150
10192
  "symbol.style.baseline.base-level",
10151
10193
  "symbol.style.baseline.width",
10194
+ "symbol.animation",
10195
+ "symbol.animation.price-changes",
10152
10196
  "symbol.timezone",
10153
10197
  "scales",
10154
10198
  "scales.price-scale",
@@ -10291,8 +10335,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
10291
10335
  .vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
10292
10336
  .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
10293
10337
  .vela-sd-mobile .vela-sd-btn{height:38px;}
10294
- .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
10295
- `;
10338
+ .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}`;
10296
10339
  if (!existing) document.head.appendChild(st);
10297
10340
  }
10298
10341
  var SettingsDialog = class {
@@ -10313,6 +10356,8 @@ var SettingsDialog = class {
10313
10356
  this.themeControl = null;
10314
10357
  /** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
10315
10358
  this.tabs = [];
10359
+ this.hintTips = [];
10360
+ // ⓘ tooltip disposers of the open build (see hint)
10316
10361
  /** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
10317
10362
  this.typeActiveInstance = /* @__PURE__ */ new Map();
10318
10363
  /** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
@@ -10501,6 +10546,13 @@ var SettingsDialog = class {
10501
10546
  body.append(g);
10502
10547
  }
10503
10548
  showActive(config.series.style);
10549
+ body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
10550
+ body.append(sid(this.boolRow(
10551
+ "Animate price changes",
10552
+ config.priceScale.animateLastPrice,
10553
+ (v) => this.emit({ priceScale: { animateLastPrice: v } }),
10554
+ this.hint("Glide the live bar to each new price instead of snapping.")
10555
+ ), "symbol.animation.price-changes"));
10504
10556
  body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
10505
10557
  body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
10506
10558
  const renderHostSections = (placement) => {
@@ -10708,6 +10760,8 @@ var SettingsDialog = class {
10708
10760
  this.ui = null;
10709
10761
  this.root = null;
10710
10762
  this.tabs = [];
10763
+ for (const dispose of this.hintTips) dispose();
10764
+ this.hintTips = [];
10711
10765
  ui?.destroy();
10712
10766
  }
10713
10767
  destroy() {
@@ -11121,23 +11175,31 @@ var SettingsDialog = class {
11121
11175
  }).el
11122
11176
  });
11123
11177
  }
11124
- /** A toggle row: the checkbox sits to the LEFT of its label (never in the control area). */
11125
- boolRow(label, value, onChange) {
11126
- return this.toggleRow(label, value, onChange, []);
11178
+ /** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
11179
+ * `info` (see {@link hint}) rides after the label. */
11180
+ boolRow(label, value, onChange, info) {
11181
+ return this.toggleRow(label, value, onChange, [], void 0, info);
11127
11182
  }
11128
11183
  /** An enable row: checkbox + label in the label column, dependent controls in the
11129
11184
  * shared control column; the control group dims and ignores input while the toggle
11130
11185
  * is off. With `get`, the row re-reads its state on a 'vela-sync' event. */
11131
- toggleRow(label, value, onToggle, controls, get) {
11186
+ toggleRow(label, value, onToggle, controls, get, info) {
11132
11187
  return fieldRow({
11133
11188
  label,
11134
11189
  labelSize: "sm",
11135
11190
  bool: controls.length === 0,
11136
11191
  toggle: { checked: value, onChange: onToggle, get },
11137
11192
  control: controls,
11193
+ info,
11138
11194
  className: controls.length === 0 ? "vela-sd-bool" : "vela-sd-row"
11139
11195
  });
11140
11196
  }
11197
+ /** The shared ⓘ hint after a row label (see {@link chromeHint}). Disposed with the dialog. */
11198
+ hint(text) {
11199
+ const { el, dispose } = chromeHint(text, { host: this.container, theme: () => this.theme });
11200
+ this.hintTips.push(dispose);
11201
+ return el;
11202
+ }
11141
11203
  numberRow(label, value, min, max, step, onChange) {
11142
11204
  return fieldRow({
11143
11205
  label,
@@ -11317,6 +11379,18 @@ var DrawingPainter = class {
11317
11379
  }
11318
11380
  this.paintHandles(ctx, pts);
11319
11381
  }
11382
+ /** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
11383
+ * accent under a thin dashed outline. */
11384
+ paintMarquee(ctx, rect) {
11385
+ ctx.save();
11386
+ ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
11387
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
11388
+ ctx.strokeStyle = HANDLE_BORDER;
11389
+ ctx.lineWidth = 1;
11390
+ ctx.setLineDash([4, 3]);
11391
+ ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
11392
+ ctx.restore();
11393
+ }
11320
11394
  /** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
11321
11395
  paintSnapRing(ctx, x, y, theme) {
11322
11396
  ctx.setLineDash([]);
@@ -12785,6 +12859,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
12785
12859
  }
12786
12860
  return null;
12787
12861
  }
12862
+ function deletableSelection(selected, drawings) {
12863
+ const ids = [...selected];
12864
+ if (ids.length < 2) return ids;
12865
+ return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
12866
+ }
12867
+ function deleteTargets(hit, selected, drawings, withSelection) {
12868
+ if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
12869
+ return hit.locked ? [] : [hit.id];
12870
+ }
12788
12871
 
12789
12872
  // src/renderers/native/drawings/DrawingInteraction.ts
12790
12873
  var DRAG_SLOP2 = 3;
@@ -12888,10 +12971,32 @@ var DrawingInteraction = class {
12888
12971
  activeDragId() {
12889
12972
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
12890
12973
  }
12974
+ /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
12975
+ pressedId() {
12976
+ return this.state.kind === "pressed" ? this.state.id : null;
12977
+ }
12978
+ /** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
12979
+ dragClones() {
12980
+ return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
12981
+ }
12982
+ /** The marquee box being swept, normalized (or null when none is in progress). */
12983
+ marqueeRect() {
12984
+ if (this.state.kind !== "marquee") return null;
12985
+ const { x0, y0, x1, y1 } = this.state;
12986
+ return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
12987
+ }
12988
+ /** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
12989
+ * armed or a gesture is in flight (that path owns the press). */
12990
+ beginMarquee(x, y) {
12991
+ if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
12992
+ this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
12993
+ return true;
12994
+ }
12891
12995
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
12892
12996
  * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
12893
- * while placing a line tool it locks the segment angle to 45° steps. */
12894
- down(x, y, mode = "off", shift2 = false) {
12997
+ * while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
12998
+ * drawing also toggles on a click, but a body drag then moves a COPY. */
12999
+ down(x, y, mode = "off", shift2 = false, mod = false) {
12895
13000
  const proj = this.deps.projector();
12896
13001
  if (this.state.kind === "placing") {
12897
13002
  if (this.state.draft.placementMode() !== "click") return;
@@ -12919,7 +13024,50 @@ var DrawingInteraction = class {
12919
13024
  return;
12920
13025
  }
12921
13026
  const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
12922
- this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
13027
+ const riders = [];
13028
+ if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
13029
+ for (const id of this.deps.selectedIds()) {
13030
+ const d = id === hit.id ? null : this.byId(id);
13031
+ if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
13032
+ }
13033
+ }
13034
+ this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
13035
+ }
13036
+ }
13037
+ /** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
13038
+ * pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
13039
+ bodyDelta(paneId, x, y) {
13040
+ if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
13041
+ const proj = this.deps.projector();
13042
+ const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
13043
+ const to = proj.pxToPoint(x, y, paneId);
13044
+ return { dt: to.time - from.time, dp: to.price - from.price };
13045
+ }
13046
+ /** The pressed drawing and its riders, each with its anchors at the press. */
13047
+ dragSources(d) {
13048
+ if (this.state.kind !== "pressed") return [];
13049
+ const out = [{ d, orig: this.state.orig }];
13050
+ for (const r of this.state.riders) {
13051
+ const rd = this.byId(r.id);
13052
+ if (rd) out.push({ d: rd, orig: r.orig });
13053
+ }
13054
+ return out;
13055
+ }
13056
+ /** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
13057
+ * cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
13058
+ * sources that have not moved yet, so their own anchors are the origin to translate from. */
13059
+ dragBody(d, x, y) {
13060
+ if (this.state.kind !== "pressed") return;
13061
+ this.snapAt = null;
13062
+ if (this.state.mod && !this.state.clones) {
13063
+ this.state.clones = this.dragSources(d).flatMap((s) => {
13064
+ const c = deserializeDrawing(s.d.serialize());
13065
+ return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
13066
+ });
13067
+ }
13068
+ for (const t of this.state.clones ?? this.dragSources(d)) {
13069
+ const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
13070
+ t.d.anchors = t.d.translateBody(dt, dp, t.orig);
12923
13071
  }
12924
13072
  }
12925
13073
  /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
@@ -12943,12 +13091,20 @@ var DrawingInteraction = class {
12943
13091
  this.deps.changed();
12944
13092
  return;
12945
13093
  }
13094
+ if (this.state.kind === "marquee") {
13095
+ this.state.x1 = x;
13096
+ this.state.y1 = y;
13097
+ this.deps.changed();
13098
+ return;
13099
+ }
12946
13100
  if (this.state.kind !== "pressed") return;
12947
13101
  const d = this.byId(this.state.id);
12948
13102
  if (!d || d.locked) return;
12949
13103
  if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
12950
13104
  this.state.moved = true;
12951
- if (this.state.handle >= 0) {
13105
+ if (this.state.handle < 0) {
13106
+ this.dragBody(d, x, y);
13107
+ } else {
12952
13108
  const a = d.anchors[this.state.handle];
12953
13109
  if (a) {
12954
13110
  const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
@@ -12956,12 +13112,6 @@ var DrawingInteraction = class {
12956
13112
  applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
12957
13113
  }
12958
13114
  d.constrainHandleDrag(this.state.handle);
12959
- } else {
12960
- const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
12961
- this.snapAt = null;
12962
- const dt = pt.time - this.state.grab.time;
12963
- const dp = pt.price - this.state.grab.price;
12964
- d.anchors = d.translateBody(dt, dp, this.state.orig);
12965
13115
  }
12966
13116
  this.deps.changed();
12967
13117
  }
@@ -12981,16 +13131,46 @@ var DrawingInteraction = class {
12981
13131
  }
12982
13132
  return;
12983
13133
  }
13134
+ if (this.state.kind === "marquee") {
13135
+ const rect = this.marqueeRect();
13136
+ this.state = { kind: "idle" };
13137
+ if (rect.w > DRAG_SLOP2 || rect.h > DRAG_SLOP2) this.selectWithin(rect);
13138
+ this.deps.changed();
13139
+ return;
13140
+ }
12984
13141
  if (this.state.kind !== "pressed") return;
12985
- const { id, moved } = this.state;
13142
+ const { id, moved, riders, mod, clones } = this.state;
12986
13143
  this.state = { kind: "idle" };
12987
13144
  const d = this.byId(id);
12988
- if (moved) {
12989
- if (d) this.deps.emit({ kind: "edit", doc: d.serialize() });
12990
- this.deps.changed();
12991
- } else {
12992
- this.deps.openSettings(id, x, y);
13145
+ if (!moved) {
13146
+ if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
13147
+ else this.deps.openSettings(id, x, y);
13148
+ return;
12993
13149
  }
13150
+ if (clones) {
13151
+ this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
13152
+ } else if (d) {
13153
+ const docs = [d.serialize()];
13154
+ for (const r of riders) {
13155
+ const rd = this.byId(r.id);
13156
+ if (rd) docs.push(rd.serialize());
13157
+ }
13158
+ if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
13159
+ else this.deps.emit({ kind: "edit", doc: docs[0] });
13160
+ }
13161
+ this.deps.changed();
13162
+ }
13163
+ /** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
13164
+ * so successive sweeps accumulate). No change when the box touches nothing new. */
13165
+ selectWithin(rect) {
13166
+ const proj = this.deps.projector();
13167
+ const ids = [...this.deps.selectedIds()];
13168
+ for (const d of this.deps.drawings()) {
13169
+ if (!d.visible || ids.includes(d.id)) continue;
13170
+ const b = d.bounds(proj);
13171
+ if (b && rectsIntersect(rect, b)) ids.push(d.id);
13172
+ }
13173
+ if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
12994
13174
  }
12995
13175
  /** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
12996
13176
  cancel() {
@@ -13003,8 +13183,15 @@ var DrawingInteraction = class {
13003
13183
  return true;
13004
13184
  }
13005
13185
  if (this.state.kind === "pressed") {
13006
- const d = this.byId(this.state.id);
13007
- if (d && this.state.moved) d.anchors = this.state.orig.map((o) => ({ time: o.time, price: o.price }));
13186
+ if (this.state.moved && !this.state.clones) {
13187
+ const d = this.byId(this.state.id);
13188
+ if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
13189
+ }
13190
+ this.state = { kind: "idle" };
13191
+ this.deps.changed();
13192
+ return true;
13193
+ }
13194
+ if (this.state.kind === "marquee") {
13008
13195
  this.state = { kind: "idle" };
13009
13196
  this.deps.changed();
13010
13197
  return true;
@@ -13119,6 +13306,9 @@ var DrawingInteraction = class {
13119
13306
  function cloneAnchors(d) {
13120
13307
  return d.anchors.map((a) => ({ time: a.time, price: a.price }));
13121
13308
  }
13309
+ function rectsIntersect(a, b) {
13310
+ return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
13311
+ }
13122
13312
  function applyFree(anchor, pt, free) {
13123
13313
  if (free === "both" || free === "x") anchor.time = pt.time;
13124
13314
  if (free === "both" || free === "y") anchor.price = pt.price;
@@ -13504,10 +13694,27 @@ function btn(label, primary, onClick) {
13504
13694
  }
13505
13695
 
13506
13696
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
13697
+ var MIXED = /* @__PURE__ */ Symbol("mixed");
13698
+ function sharedPaths(drawings) {
13699
+ const [first, ...rest] = drawings;
13700
+ const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
13701
+ for (const d of rest) {
13702
+ const own = new Set(d.schema().fields.map((f) => f.path));
13703
+ for (const p of paths) if (!own.has(p)) paths.delete(p);
13704
+ }
13705
+ return paths;
13706
+ }
13707
+ function commonValue(drawings, read) {
13708
+ const first = read(drawings[0]);
13709
+ return drawings.every((d) => read(d) === first) ? first : MIXED;
13710
+ }
13711
+ function distinctValues(drawings, read) {
13712
+ return [...new Set(drawings.map(read))];
13713
+ }
13507
13714
  var BTN = 30;
13508
13715
  var ICON = 21;
13509
13716
  var STYLE_ID4 = "vela-drawing-popup-styles";
13510
- var STYLE_REV = "3";
13717
+ var STYLE_REV = "4";
13511
13718
  function ensureStyles4() {
13512
13719
  if (typeof document === "undefined") return;
13513
13720
  const existing = document.getElementById(STYLE_ID4);
@@ -13519,6 +13726,8 @@ function ensureStyles4() {
13519
13726
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
13520
13727
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
13521
13728
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
13729
+ /* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
13730
+ .vela-dpop-btn[data-active='mixed']{background:var(--vela-hover-strong);color:var(--vela-fg-bright);outline:1px dashed var(--vela-fg-muted);outline-offset:-2px;}
13522
13731
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
13523
13732
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
13524
13733
  .vela-dpop-item[data-active='1']{background:var(--vela-active);}
@@ -13550,7 +13759,7 @@ var DrawingSettingsPopup = class {
13550
13759
  if (this.isOwnChrome(node)) return;
13551
13760
  const cb = this.onClose;
13552
13761
  this.close();
13553
- cb?.();
13762
+ cb?.(e);
13554
13763
  };
13555
13764
  this.theme = theme;
13556
13765
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
@@ -13575,16 +13784,27 @@ var DrawingSettingsPopup = class {
13575
13784
  contains(node) {
13576
13785
  return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
13577
13786
  }
13578
- /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
13579
- * `onClose` fires on an outside-click dismissal (not a programmatic close). */
13580
- open(drawing, anchor, actions, onClose) {
13787
+ /** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
13788
+ * their `anchor` box. With several drawings the bar shows only the controls they ALL support,
13789
+ * a control whose values differ reads as mixed, and every edit applies to all of them —
13790
+ * per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
13791
+ * `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
13792
+ * programmatic close), with the dismissing press when there is one, so the caller can tell a
13793
+ * modifier press (multi-select) from a plain one. */
13794
+ open(drawings, anchor, actions, onClose) {
13581
13795
  this.close();
13796
+ const drawing = drawings[0];
13797
+ if (!drawing) return;
13582
13798
  ensureStyles4();
13583
13799
  this.onClose = onClose ?? null;
13584
13800
  const t = this.theme;
13801
+ const multi = drawings.length > 1;
13585
13802
  const schema = drawing.schema();
13586
- const paths = new Set(schema.fields.map((f) => f.path));
13587
- const textOnBar = schema.textIsContent === true || !paths.has("text.value");
13803
+ const paths = sharedPaths(drawings);
13804
+ const common = (read) => commonValue(drawings, read);
13805
+ const every = (pred) => drawings.every(pred);
13806
+ const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
13807
+ const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
13588
13808
  const el = document.createElement("div");
13589
13809
  el.className = "vela-dpop";
13590
13810
  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;`;
@@ -13598,75 +13818,84 @@ var DrawingSettingsPopup = class {
13598
13818
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
13599
13819
  bar.appendChild(this.dragHandle());
13600
13820
  if (paths.has("glyph")) {
13601
- const cur = drawing.glyph ?? GLYPH_OPTIONS[0];
13821
+ const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
13602
13822
  bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
13603
13823
  }
13604
13824
  if (paths.has("size")) {
13605
- const sz = drawing.size ?? "normal";
13825
+ const sz = common((d) => d.size ?? "normal");
13606
13826
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
13607
13827
  }
13608
- if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
13828
+ if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
13829
+ const mag = (d) => d;
13609
13830
  const options = this.lowerTimeframeOptions();
13610
13831
  if (options.length > 0) {
13611
13832
  bar.appendChild(
13612
- this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
13833
+ this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
13613
13834
  label: (v) => magnifierTimeframeLabel(String(v)),
13614
13835
  labelInTrigger: true
13615
13836
  })
13616
13837
  );
13617
13838
  }
13618
- bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
13619
- bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
13839
+ bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
13840
+ bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
13620
13841
  }
13621
- if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || (drawing instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), (v) => actions.patch({ "style.lineColor": v })));
13842
+ if (paths.has("style.lineColor")) bar.appendChild(swatch("Line color", BRUSH_ICON, (d) => d.style.lineColor || (d instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), "style.lineColor"));
13622
13843
  if (paths.has("style.lineWidth")) {
13623
- const wf = schema.fields.find((f) => f.path === "style.lineWidth");
13624
- if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
13625
- bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
13844
+ const widthField = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
13845
+ const wf = widthField(drawing);
13846
+ const width = common((d) => d.style.lineWidth);
13847
+ const markers = every((d) => {
13848
+ const f = widthField(d);
13849
+ return f?.kind === "number" && (f.min ?? 1) > 1;
13850
+ });
13851
+ if (markers && wf?.kind === "number") {
13852
+ bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
13626
13853
  } else {
13627
- bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], drawing.style.lineWidth, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
13854
+ bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], width, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
13628
13855
  }
13629
13856
  }
13630
- if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), drawing.style.lineStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
13631
- if (paths.has("style.fillColor")) bar.appendChild(this.colorButton("Fill", BUCKET_ICON, effectiveFillColor(drawing, this.theme) ?? drawing.style.fillColor ?? DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.fillColor": v })));
13857
+ if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), common((d) => d.style.lineStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
13858
+ if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
13632
13859
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
13633
- const isPosition = paths.has("riskPercent") && drawing instanceof PositionTool;
13860
+ const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
13634
13861
  if (isPosition) {
13635
- const pos = drawing;
13636
- bar.appendChild(this.colorButton("Profit zone", BUCKET_ICON, pos.profitColor, (v) => actions.patch({ profitColor: v })));
13637
- bar.appendChild(this.colorButton("Loss zone", BUCKET_ICON, pos.lossColor, (v) => actions.patch({ lossColor: v })));
13862
+ const pos = (d) => d;
13863
+ bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
13864
+ bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
13638
13865
  }
13639
- const reg = drawing.reg;
13640
- if (paths.has("reg.midColor") && reg) {
13866
+ const regOf = (d) => d.reg;
13867
+ if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
13868
+ const reg = (d) => regOf(d);
13641
13869
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
13642
- bar.appendChild(this.colorButton("Midline color", BRUSH_ICON, reg.midColor, (v) => actions.patch({ "reg.midColor": v })));
13643
- bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
13644
- bar.appendChild(this.colorButton("Upper line color", BRUSH_ICON, reg.upperColor, (v) => actions.patch({ "reg.upperColor": v })));
13645
- bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
13646
- bar.appendChild(this.colorButton("Lower line color", BRUSH_ICON, reg.lowerColor, (v) => actions.patch({ "reg.lowerColor": v })));
13647
- bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
13648
- bar.appendChild(this.colorButton("Upper fill", BUCKET_ICON, reg.upperFill, (v) => actions.patch({ "reg.upperFill": v })));
13649
- bar.appendChild(this.colorButton("Lower fill", BUCKET_ICON, reg.lowerFill, (v) => actions.patch({ "reg.lowerFill": v })));
13650
- bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
13651
- }
13652
- const vwap2 = drawing.vwap;
13653
- if (paths.has("vwap.midColor") && vwap2) {
13870
+ bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
13871
+ bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
13872
+ bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
13873
+ bar.appendChild(this.dropdown("Upper line style", styles, common((d) => reg(d).upperStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
13874
+ bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
13875
+ bar.appendChild(this.dropdown("Lower line style", styles, common((d) => reg(d).lowerStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
13876
+ bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
13877
+ bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
13878
+ bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
13879
+ }
13880
+ const vwapOf = (d) => d.vwap;
13881
+ if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
13882
+ const vwap2 = (d) => vwapOf(d);
13654
13883
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
13655
13884
  const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
13656
- bar.appendChild(this.colorButton("VWAP color", BRUSH_ICON, vwap2.midColor, (v) => actions.patch({ "vwap.midColor": v })));
13657
- bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
13885
+ bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
13886
+ bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
13658
13887
  bar.appendChild(
13659
- this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
13888
+ this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
13660
13889
  label: (v) => `${v}\u03C3`,
13661
13890
  labelInTrigger: true
13662
13891
  })
13663
13892
  );
13664
- bar.appendChild(this.colorButton("Upper band color", BRUSH_ICON, vwap2.upperColor, (v) => actions.patch({ "vwap.upperColor": v })));
13665
- bar.appendChild(this.colorButton("Lower band color", BRUSH_ICON, vwap2.lowerColor, (v) => actions.patch({ "vwap.lowerColor": v })));
13666
- bar.appendChild(this.colorButton("Band fill", BUCKET_ICON, vwap2.bandFill, (v) => actions.patch({ "vwap.bandFill": v })));
13893
+ bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
13894
+ bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
13895
+ bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
13667
13896
  }
13668
13897
  if (paths.has("maxCurvature")) {
13669
- const cur = drawing.maxCurvature ?? 24;
13898
+ const cur = common((d) => d.maxCurvature ?? 24);
13670
13899
  bar.appendChild(
13671
13900
  this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON, (v) => actions.patch({ maxCurvature: v }), {
13672
13901
  label: (v) => `n=${v}`,
@@ -13675,7 +13904,7 @@ var DrawingSettingsPopup = class {
13675
13904
  );
13676
13905
  }
13677
13906
  if (paths.has("mach")) {
13678
- const cur = drawing.mach ?? 2;
13907
+ const cur = common((d) => d.mach ?? 2);
13679
13908
  bar.appendChild(
13680
13909
  this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON, (v) => actions.patch({ mach: v }), {
13681
13910
  label: (v) => `M=${v}`,
@@ -13684,7 +13913,7 @@ var DrawingSettingsPopup = class {
13684
13913
  );
13685
13914
  }
13686
13915
  if (paths.has("waveCount")) {
13687
- const cur = drawing.waveCount ?? 6;
13916
+ const cur = common((d) => d.waveCount ?? 6);
13688
13917
  bar.appendChild(
13689
13918
  this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON, (v) => actions.patch({ waveCount: v }), {
13690
13919
  label: (v) => `${v}`,
@@ -13692,26 +13921,27 @@ var DrawingSettingsPopup = class {
13692
13921
  })
13693
13922
  );
13694
13923
  }
13695
- const toggles = drawing;
13696
- if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, toggles.showPrice !== false, (v) => actions.patch({ showPrice: v })));
13697
- if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, toggles.showDate !== false, (v) => actions.patch({ showDate: v })));
13698
- if (paths.has("text.color") && textOnBar) bar.appendChild(this.colorButton("Text color", TYPE_ICON, drawing.text?.color || t.textColor, (v) => actions.patch({ "text.color": v })));
13699
- if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), drawing.text?.size ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
13700
- if (paths.has("text.bold") && !paths.has("text.value")) bar.appendChild(this.toggle("Bold", BOLD_ICON, !!drawing.text?.bold, (v) => actions.patch({ "text.bold": v })));
13701
- if (paths.has("text.italic") && !paths.has("text.value")) bar.appendChild(this.toggle("Italic", ITALIC_ICON, !!drawing.text?.italic, (v) => actions.patch({ "text.italic": v })));
13702
- if (paths.has("text.value")) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
13924
+ const flags = (d) => d;
13925
+ if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
13926
+ if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
13927
+ if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
13928
+ if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
13929
+ const textField = !multi && paths.has("text.value");
13930
+ if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
13931
+ if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
13932
+ if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
13703
13933
  const editableLevels = drawing.editableLevels();
13704
- if (editableLevels && !(drawing instanceof MachFigure)) {
13705
- const fib = drawing;
13934
+ if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
13935
+ const fib = (d) => d;
13706
13936
  const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
13707
- bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
13708
- bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
13937
+ bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
13938
+ bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
13709
13939
  }
13710
13940
  bar.appendChild(this.divider());
13711
- if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
13712
- if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
13713
- if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
13714
- bar.appendChild(this.toggle("Lock", LOCK_ICON, drawing.locked, (v) => actions.setLocked(v)));
13941
+ if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
13942
+ if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
13943
+ if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
13944
+ bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
13715
13945
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
13716
13946
  del.style.color = "var(--vela-danger)";
13717
13947
  bar.appendChild(del);
@@ -13897,7 +14127,7 @@ var DrawingSettingsPopup = class {
13897
14127
  });
13898
14128
  return b;
13899
14129
  }
13900
- /** The trailing kebab overflow — z-order actions + reset settings. */
14130
+ /** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
13901
14131
  kebabButton(actions) {
13902
14132
  const b = this.base("More");
13903
14133
  b.innerHTML = sized(KEBAB_ICON);
@@ -13909,6 +14139,7 @@ var DrawingSettingsPopup = class {
13909
14139
  return;
13910
14140
  }
13911
14141
  this.openActionMenu(b, [
14142
+ { icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
13912
14143
  { icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
13913
14144
  { icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
13914
14145
  { icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
@@ -14065,26 +14296,30 @@ var DrawingSettingsPopup = class {
14065
14296
  this.tipEl?.remove();
14066
14297
  this.tipEl = null;
14067
14298
  }
14068
- toggle(tip, icon2, active, onChange) {
14299
+ /** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
14300
+ * the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
14301
+ * toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
14302
+ toggle(tip, icon2, active, onChange, off) {
14069
14303
  const b = this.base(tip);
14070
- b.innerHTML = sized(icon2);
14071
14304
  const set = (on2) => {
14072
- b.dataset.active = on2 ? "1" : "0";
14305
+ b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
14306
+ b.innerHTML = sized(off && on2 === false ? off : icon2);
14073
14307
  };
14074
14308
  set(active);
14075
14309
  let on = active;
14076
14310
  b.addEventListener("click", () => {
14077
- on = !on;
14311
+ on = on === MIXED ? true : !on;
14078
14312
  set(on);
14079
14313
  onChange(on);
14080
14314
  });
14081
14315
  return b;
14082
14316
  }
14083
14317
  /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
14084
- * ladder — the value is clamped to the schema's declared min/max on commit. */
14318
+ * ladder — the value is clamped to the schema's declared min/max on commit. A mixed
14319
+ * value shows an empty field with a dash until a number is typed for all. */
14085
14320
  widthInput(tip, value, min, max, step, onChange) {
14086
14321
  const ni = new NumberInput({
14087
- value,
14322
+ value: value === MIXED ? min : value,
14088
14323
  min,
14089
14324
  max,
14090
14325
  step,
@@ -14096,13 +14331,18 @@ var DrawingSettingsPopup = class {
14096
14331
  title: tip,
14097
14332
  onChange
14098
14333
  });
14334
+ if (value === MIXED) {
14335
+ ni.input.value = "";
14336
+ ni.input.placeholder = "\u2014";
14337
+ }
14099
14338
  ni.el.dataset.tip = tip;
14100
14339
  trapChartKeys(ni.el);
14101
14340
  return ni.el;
14102
14341
  }
14103
14342
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
14104
14343
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
14105
- * opens a floating list of every option so a value is one click away (no cycling through). */
14344
+ * opens a floating list of every option so a value is one click away (no cycling through).
14345
+ * A mixed value shows a dash in the trigger and highlights no row. */
14106
14346
  dropdown(tip, values, current, render, onChange, opts = {}) {
14107
14347
  const b = this.base(tip);
14108
14348
  b.style.width = "auto";
@@ -14112,14 +14352,20 @@ var DrawingSettingsPopup = class {
14112
14352
  let cur = current;
14113
14353
  const paint = (v) => {
14114
14354
  b.replaceChildren();
14115
- const glyph = render(v);
14355
+ if (v === MIXED) {
14356
+ const dash = document.createElement("span");
14357
+ dash.textContent = "\u2014";
14358
+ dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
14359
+ b.appendChild(dash);
14360
+ }
14361
+ const glyph = v === MIXED ? "" : render(v);
14116
14362
  if (glyph) {
14117
14363
  const ic = document.createElement("span");
14118
14364
  ic.style.cssText = "display:flex;";
14119
14365
  ic.innerHTML = sized(glyph);
14120
14366
  b.appendChild(ic);
14121
14367
  }
14122
- if (opts.label && opts.labelInTrigger) {
14368
+ if (opts.label && opts.labelInTrigger && v !== MIXED) {
14123
14369
  const tx = document.createElement("span");
14124
14370
  tx.textContent = opts.label(v);
14125
14371
  tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
@@ -14189,8 +14435,10 @@ var DrawingSettingsPopup = class {
14189
14435
  }
14190
14436
  /** A color control: an `icon` over a thin **colored underline** showing the current
14191
14437
  * value (the common idiom), with an invisible native picker overlaid to edit it.
14192
- * `iconSize` shrinks it for the floating text controls. */
14193
- colorButton(tip, icon2, color, onChange, iconSize = 17) {
14438
+ * `iconSize` shrinks it for the floating text controls. A mixed color paints the
14439
+ * underline striped with the colors `used` across the selection, and the picker then
14440
+ * leads with those colors so unifying onto one of them is a single click. */
14441
+ colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
14194
14442
  const b = document.createElement("button");
14195
14443
  b.type = "button";
14196
14444
  b.dataset.tip = tip;
@@ -14200,13 +14448,14 @@ var DrawingSettingsPopup = class {
14200
14448
  ic.style.cssText = "display:flex;";
14201
14449
  ic.innerHTML = sized(icon2, iconSize);
14202
14450
  const bar = document.createElement("span");
14203
- bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
14451
+ bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
14204
14452
  b.append(ic, bar);
14205
14453
  let cur = color;
14206
14454
  b.addEventListener("pointerdown", (e) => e.stopPropagation());
14207
14455
  b.addEventListener("click", (e) => {
14208
14456
  e.stopPropagation();
14209
- this.toggleColorPopover(b, cur, (v) => {
14457
+ const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
14458
+ this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
14210
14459
  cur = v;
14211
14460
  bar.style.background = v;
14212
14461
  onChange(v);
@@ -14215,20 +14464,42 @@ var DrawingSettingsPopup = class {
14215
14464
  return b;
14216
14465
  }
14217
14466
  /** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
14218
- toggleColorPopover(anchor, color, onChange) {
14467
+ toggleColorPopover(anchor, color, used, onChange) {
14219
14468
  if (this.colorOwner === anchor) {
14220
14469
  this.closeColorPopover();
14221
14470
  return;
14222
14471
  }
14223
- this.openColorPopover(anchor, color, onChange);
14472
+ this.openColorPopover(anchor, color, used, onChange);
14224
14473
  }
14225
- /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
14226
- openColorPopover(anchor, color, onChange) {
14474
+ /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
14475
+ * `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
14476
+ openColorPopover(anchor, color, used, onChange) {
14227
14477
  const t = this.theme;
14228
14478
  this.colorPop = this.hostFloat(anchor, {
14229
14479
  zIndex: 25,
14230
14480
  padding: "10px",
14231
- fill: (el) => {
14481
+ fill: (el, pop) => {
14482
+ if (used.length > 1) {
14483
+ const row = document.createElement("div");
14484
+ row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
14485
+ const lbl = document.createElement("span");
14486
+ lbl.textContent = "In use";
14487
+ lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
14488
+ row.appendChild(lbl);
14489
+ for (const c of used) {
14490
+ const sw = document.createElement("button");
14491
+ sw.type = "button";
14492
+ sw.dataset.tip = c;
14493
+ sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
14494
+ sw.addEventListener("click", (e) => {
14495
+ e.stopPropagation();
14496
+ pop.hide();
14497
+ onChange(c);
14498
+ });
14499
+ row.appendChild(sw);
14500
+ }
14501
+ el.appendChild(row);
14502
+ }
14232
14503
  el.appendChild(buildColorPicker(color, t, onChange));
14233
14504
  }
14234
14505
  });
@@ -14249,6 +14520,7 @@ var TYPE_ICON = icon("type");
14249
14520
  var PRICE_DELTA_ICON = icon("price-delta");
14250
14521
  var DATE_DELTA_ICON = icon("date-delta");
14251
14522
  var LOCK_ICON = icon("lock");
14523
+ var UNLOCK_ICON = icon("unlock");
14252
14524
  var FRONT_ICON = icon("bring-front");
14253
14525
  var BACK_ICON = icon("send-back");
14254
14526
  var TRASH_ICON = icon("trash");
@@ -14256,6 +14528,7 @@ var BOLD_ICON = icon("bold");
14256
14528
  var ITALIC_ICON = icon("italic");
14257
14529
  var GRIP_ICON = icon("grip");
14258
14530
  var KEBAB_ICON = icon("kebab");
14531
+ var CLONE_ICON = icon("clone");
14259
14532
  var RESET_ICON = icon("reset");
14260
14533
  var GEAR_ICON = icon("gear");
14261
14534
  var CHEVRON_ICON = icon("chevron-down");
@@ -14264,6 +14537,12 @@ var BANDS_ICON = icon("bands");
14264
14537
  var DEDEKIND_ICON = icon("dedekind");
14265
14538
  var SONIC_ICON = icon("sonic");
14266
14539
  var SUPERSONIC_ICON = icon("supersonic");
14540
+ function stripes(colors) {
14541
+ const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
14542
+ const step = 100 / bands.length;
14543
+ const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
14544
+ return `linear-gradient(90deg,${stops})`;
14545
+ }
14267
14546
  function lineIcon(width, style) {
14268
14547
  const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
14269
14548
  return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
@@ -14547,6 +14826,20 @@ var UserDrawingController = class {
14547
14826
  // reported exactly when it actually changes).
14548
14827
  this.textEditor = null;
14549
14828
  this.painter = new DrawingPainter();
14829
+ /** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
14830
+ * shrinking selection does with it. */
14831
+ this.popupMulti = false;
14832
+ /** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
14833
+ * selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
14834
+ * selection or sweep a marquee onto it), or it landed on a drawing that is already selected
14835
+ * (the canvas handler ran first and is holding it for a drag or a click), so the drawings
14836
+ * whose bar just closed must stay selected. */
14837
+ this.onPopupDismissed = (e) => {
14838
+ if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
14839
+ const pressed = this.interaction.pressedId();
14840
+ if (pressed && this.selectedIds.has(pressed)) return;
14841
+ this.clearSelection();
14842
+ };
14550
14843
  this.ctx = canvas.getContext("2d");
14551
14844
  this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
14552
14845
  this.toolbar = new DrawingToolbar(
@@ -14723,6 +15016,12 @@ var UserDrawingController = class {
14723
15016
  }
14724
15017
  setSelection(ids) {
14725
15018
  this.selectedIds = new Set(ids);
15019
+ if (ids.length >= 2) this.openSettingsForSelection(ids);
15020
+ else if (this.popupMulti) {
15021
+ this.popupMulti = false;
15022
+ this.popup.close();
15023
+ if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
15024
+ }
14726
15025
  this.render();
14727
15026
  }
14728
15027
  /** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
@@ -14768,12 +15067,16 @@ var UserDrawingController = class {
14768
15067
  });
14769
15068
  }
14770
15069
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
14771
- * Shared by the eraser (click + drag) and the middle-click shortcut. */
14772
- deleteAt(x, y) {
15070
+ * Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
15071
+ * `withSelection` so a hit on a member of a multi-selection removes the selection's
15072
+ * unlocked members — whichever member was hit, locked or not. */
15073
+ deleteAt(x, y, withSelection = false) {
14773
15074
  const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
14774
- if (!hit || hit.locked) return false;
15075
+ if (!hit) return false;
15076
+ const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
15077
+ if (ids.length === 0) return false;
14775
15078
  this.popup.close();
14776
- this.emit({ kind: "delete", ids: [hit.id] });
15079
+ this.emit({ kind: "delete", ids });
14777
15080
  return true;
14778
15081
  }
14779
15082
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
@@ -14799,7 +15102,14 @@ var UserDrawingController = class {
14799
15102
  this.render();
14800
15103
  }
14801
15104
  }
14802
- pointerDown(x, y, snap = "off", shift2 = false) {
15105
+ /** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
15106
+ * the selection). Returns false when a mode/tool is active (the normal press path owns it). */
15107
+ beginMarqueeAt(x, y) {
15108
+ if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
15109
+ this.render();
15110
+ return true;
15111
+ }
15112
+ pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
14803
15113
  if (this.eraserMode) {
14804
15114
  this.erasing = true;
14805
15115
  this.deleteAt(x, y);
@@ -14819,9 +15129,9 @@ var UserDrawingController = class {
14819
15129
  return;
14820
15130
  }
14821
15131
  }
14822
- this.interaction.down(x, y, snap, shift2);
15132
+ this.interaction.down(x, y, snap, shift2, mod);
14823
15133
  }
14824
- pointerMove(x, y, snap = "off", shift2 = false) {
15134
+ pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
14825
15135
  if (this.eraserMode) {
14826
15136
  if (this.erasing) this.deleteAt(x, y);
14827
15137
  return;
@@ -14833,12 +15143,14 @@ var UserDrawingController = class {
14833
15143
  return;
14834
15144
  }
14835
15145
  this.interaction.move(x, y, snap, shift2);
14836
- this.updateHover(x, y);
15146
+ this.updateHover(x, y, mod);
14837
15147
  }
14838
- /** Track which drawing is hovered so its handles appear on hover (and only then). */
14839
- updateHover(x, y) {
15148
+ /** Track which drawing is hovered so its handles appear on hover (and only then). While
15149
+ * Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
15150
+ * selected, so a hovered candidate doesn't already read as selected. */
15151
+ updateHover(x, y, mod = false) {
14840
15152
  let id = null;
14841
- if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
15153
+ if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
14842
15154
  id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
14843
15155
  }
14844
15156
  if (id !== this.hoveredId) {
@@ -14858,7 +15170,12 @@ var UserDrawingController = class {
14858
15170
  this.render();
14859
15171
  return;
14860
15172
  }
15173
+ const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
14861
15174
  this.interaction.up(x, y);
15175
+ if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
15176
+ if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
15177
+ else this.openSettingsById(dragged, x, y);
15178
+ }
14862
15179
  }
14863
15180
  /** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
14864
15181
  toggleMeasure() {
@@ -15123,7 +15440,7 @@ var UserDrawingController = class {
15123
15440
  this.emit({ kind: "duplicate", ids: this.selectionIds() });
15124
15441
  break;
15125
15442
  case "delete": {
15126
- const ids = this.selectedIds.size ? this.selectionIds() : this.hoveredId ? [this.hoveredId] : [];
15443
+ const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
15127
15444
  if (ids.length) {
15128
15445
  this.popup.close();
15129
15446
  this.emit({ kind: "delete", ids });
@@ -15139,6 +15456,9 @@ var UserDrawingController = class {
15139
15456
  selectionIds() {
15140
15457
  return [...this.selectedIds];
15141
15458
  }
15459
+ deletableSelection() {
15460
+ return deletableSelection(this.selectedIds, this.drawings);
15461
+ }
15142
15462
  /** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
15143
15463
  nudgeSelection(dx, dy) {
15144
15464
  const proj = this.deps.projector();
@@ -15247,6 +15567,8 @@ var UserDrawingController = class {
15247
15567
  this.painter.seriesLook = this.deps.seriesLook();
15248
15568
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
15249
15569
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
15570
+ const clones = this.interaction.dragClones();
15571
+ if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
15250
15572
  this.layoutTextEditor();
15251
15573
  const ghost = this.interaction.ghost();
15252
15574
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
@@ -15262,6 +15584,8 @@ var UserDrawingController = class {
15262
15584
  const my = m ? proj.yOf(m.point.price, m.paneId) : null;
15263
15585
  if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
15264
15586
  if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
15587
+ const marquee = this.interaction.marqueeRect();
15588
+ if (marquee) this.painter.paintMarquee(ctx, marquee);
15265
15589
  }
15266
15590
  destroy() {
15267
15591
  this.closeTextEditor();
@@ -15279,15 +15603,80 @@ var UserDrawingController = class {
15279
15603
  if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
15280
15604
  this.render();
15281
15605
  }
15606
+ /** Report the live drawings' current state as one edit (one undo step, however many). */
15607
+ emitEdits(drawings) {
15608
+ const docs = drawings.map((d) => d.serialize());
15609
+ if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
15610
+ else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
15611
+ }
15612
+ /** The one bar for a multi-selection: it shows the controls all of its drawings share and
15613
+ * every action applies to all of them. Floats clear of their combined bounds. */
15614
+ openSettingsForSelection(ids) {
15615
+ const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
15616
+ const drawings = live();
15617
+ if (drawings.length < 2) return;
15618
+ const proj = this.deps.projector();
15619
+ let anchor = null;
15620
+ for (const d of drawings) {
15621
+ const b = d.bounds(proj);
15622
+ if (!b) continue;
15623
+ if (!anchor) anchor = { ...b };
15624
+ else {
15625
+ const right = Math.max(anchor.x + anchor.w, b.x + b.w);
15626
+ const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
15627
+ anchor.x = Math.min(anchor.x, b.x);
15628
+ anchor.y = Math.min(anchor.y, b.y);
15629
+ anchor.w = right - anchor.x;
15630
+ anchor.h = bottom - anchor.y;
15631
+ }
15632
+ }
15633
+ this.popupMulti = true;
15634
+ this.emit({ kind: "settings", id: drawings[0].id });
15635
+ this.popup.open(drawings, anchor, {
15636
+ resolve: () => live()[0] ?? null,
15637
+ patch: (p) => {
15638
+ const ds = live();
15639
+ for (const d of ds) d.applySettings(p);
15640
+ this.render();
15641
+ this.emitEdits(ds);
15642
+ },
15643
+ setLocked: (v) => {
15644
+ const ds = live();
15645
+ for (const d of ds) d.locked = v;
15646
+ this.emitEdits(ds);
15647
+ },
15648
+ reorder: (to) => {
15649
+ for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
15650
+ },
15651
+ duplicate: () => {
15652
+ this.popup.close();
15653
+ this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
15654
+ },
15655
+ resetSettings: () => {
15656
+ const ds = live();
15657
+ for (const d of ds) resetDrawingSettings(d);
15658
+ this.render();
15659
+ this.emitEdits(ds);
15660
+ this.openSettingsForSelection(ids);
15661
+ },
15662
+ remove: () => {
15663
+ const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
15664
+ if (ids2.length === 0) return;
15665
+ this.popup.close();
15666
+ this.emit({ kind: "delete", ids: ids2 });
15667
+ }
15668
+ }, this.onPopupDismissed);
15669
+ }
15282
15670
  /** A click on a drawing opens its settings toolbar (text labels edit text here too). */
15283
15671
  openSettingsById(id, _x, _y) {
15284
15672
  const drawing = this.drawings.find((d) => d.id === id);
15285
15673
  if (!drawing) return;
15286
15674
  const live = () => this.drawings.find((d) => d.id === id);
15675
+ this.popupMulti = false;
15287
15676
  this.emit({ kind: "select", ids: [id] });
15288
15677
  this.emit({ kind: "settings", id });
15289
15678
  const anchor = drawing.bounds(this.deps.projector());
15290
- this.popup.open(drawing, anchor, {
15679
+ this.popup.open([drawing], anchor, {
15291
15680
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
15292
15681
  // position tool's price fields, where one edit can flip another level) resolves fresh.
15293
15682
  resolve: () => live() ?? null,
@@ -15305,6 +15694,11 @@ var UserDrawingController = class {
15305
15694
  this.emit({ kind: "edit", doc: d.serialize() });
15306
15695
  },
15307
15696
  reorder: (to) => this.emit({ kind: "reorder", id, to }),
15697
+ duplicate: () => {
15698
+ this.finishTextEditor(true);
15699
+ this.popup.close();
15700
+ this.emit({ kind: "duplicate", ids: [id] });
15701
+ },
15308
15702
  resetSettings: () => {
15309
15703
  const d = live();
15310
15704
  if (!d) return;
@@ -15327,7 +15721,13 @@ var UserDrawingController = class {
15327
15721
  this.popup.close();
15328
15722
  this.emit({ kind: "delete", ids: [id] });
15329
15723
  }
15330
- }, () => this.clearSelection());
15724
+ }, this.onPopupDismissed);
15725
+ }
15726
+ /** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
15727
+ * does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
15728
+ * selection whose bar was dismissed by a modifier press) — this covers those. */
15729
+ deselect() {
15730
+ this.clearSelection();
15331
15731
  }
15332
15732
  /** Report placement progress upstream (`draft` intent) so the drawings sync can
15333
15733
  * mirror the ghost live on linked charts. Called from every render; only actual
@@ -15353,6 +15753,13 @@ var UserDrawingController = class {
15353
15753
  }, 0);
15354
15754
  return;
15355
15755
  }
15756
+ if (i.kind === "clone") {
15757
+ const before = new Set(this.drawings.map((d) => d.id));
15758
+ this.intentCb?.(i);
15759
+ const fresh = this.drawings.filter((d) => !before.has(d.id));
15760
+ if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
15761
+ return;
15762
+ }
15356
15763
  this.intentCb?.(i);
15357
15764
  }
15358
15765
  };
@@ -16491,7 +16898,6 @@ var ZOOM_OUT_MARGIN_BARS = 6;
16491
16898
  var ZOOM_TAU_MS = 70;
16492
16899
  var SCALE_TAU_MS = 80;
16493
16900
  var FLING_TAU_MS = 110;
16494
- var LIVE_BAR_TAU_MS = 90;
16495
16901
  var SCROLL_TO_TAU_MS = 130;
16496
16902
  var FLING_STOP_PX = 0.02;
16497
16903
  var PRICE_SCALE_K = 4e-3;
@@ -16562,9 +16968,13 @@ var NativeRenderer = class {
16562
16968
  /** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
16563
16969
  this.historyChordsEnabled = true;
16564
16970
  this.liveRegion = null;
16565
- // ── animation state (eased zoom + inertial pan) ──
16971
+ // ── animation state (eased zoom + inertial pan + live-bar glide) ──
16566
16972
  this.animZoom = true;
16567
16973
  this.animPan = true;
16974
+ this.animLiveBarMs = 0;
16975
+ // forming-bar OHLC glide time-constant; 0 = each tick snaps
16976
+ this.animLiveBarOnMs = LIVE_BAR_EASE_DEFAULT_MS;
16977
+ // the duration the settings-dialog on/off toggle restores (last non-zero value configured)
16568
16978
  // Brand default candles.
16569
16979
  this.candleUp = BULLISH;
16570
16980
  this.candleDown = BEARISH;
@@ -16689,7 +17099,7 @@ var NativeRenderer = class {
16689
17099
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
16690
17100
  this.priceStyleCbs = /* @__PURE__ */ new Set();
16691
17101
  this.name = "native";
16692
- this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
17102
+ this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
16693
17103
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
16694
17104
  * so moving onto the button doesn't count as leaving). */
16695
17105
  this.onScrollProximityMove = (e) => {
@@ -16723,6 +17133,7 @@ var NativeRenderer = class {
16723
17133
  this.backendMode = opts.nativeBackend;
16724
17134
  this.animZoom = opts.animZoom;
16725
17135
  this.animPan = opts.animPan;
17136
+ this.setLiveBarEase(resolveLiveBarEaseMs(opts.animLiveBar));
16726
17137
  this.glowAmount = opts.glow;
16727
17138
  this.candleUp = opts.upColor;
16728
17139
  this.candleDown = opts.downColor;
@@ -16772,6 +17183,10 @@ var NativeRenderer = class {
16772
17183
  case "animPan":
16773
17184
  this.animPan = Boolean(value);
16774
17185
  return;
17186
+ case "animLiveBar":
17187
+ this.setLiveBarEase(resolveLiveBarEaseMs(value));
17188
+ return;
17189
+ // affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
16775
17190
  case "intro": {
16776
17191
  const s = value === false || value === "none" || value === "off" || value == null ? "" : String(value);
16777
17192
  this.introStyle = s;
@@ -16908,6 +17323,8 @@ var NativeRenderer = class {
16908
17323
  return this.animZoom;
16909
17324
  case "animPan":
16910
17325
  return this.animPan;
17326
+ case "animLiveBar":
17327
+ return this.animLiveBarMs;
16911
17328
  case "intro":
16912
17329
  return this.introStyle;
16913
17330
  case "zoomAnchor":
@@ -17076,7 +17493,8 @@ var NativeRenderer = class {
17076
17493
  labelsVisible: this.scene.showAxisLabels,
17077
17494
  currentPriceLine: this.scene.showPriceLine,
17078
17495
  priceLabel: this.scene.showPriceLabel,
17079
- countdown: this.scene.showCountdown
17496
+ countdown: this.scene.showCountdown,
17497
+ animateLastPrice: this.animLiveBarMs > 0
17080
17498
  },
17081
17499
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
17082
17500
  trades: {
@@ -17183,6 +17601,7 @@ var NativeRenderer = class {
17183
17601
  this.scene.showPriceLabel = next.priceScale.priceLabel;
17184
17602
  this.scene.showCountdown = next.priceScale.countdown;
17185
17603
  this.syncCountdownTimer();
17604
+ this.animLiveBarMs = next.priceScale.animateLastPrice ? this.animLiveBarOnMs : 0;
17186
17605
  s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
17187
17606
  this.scene.tradeMarkers = {
17188
17607
  visible: next.trades.visible,
@@ -17626,7 +18045,10 @@ var NativeRenderer = class {
17626
18045
  zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
17627
18046
  fling: (v) => this.fling(v),
17628
18047
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
17629
- onClick: (x) => this.handleClick(x),
18048
+ onClick: (x) => {
18049
+ this.userDrawings?.deselect();
18050
+ this.handleClick(x);
18051
+ },
17630
18052
  onAxisLongPress: (axis, x, y) => {
17631
18053
  for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
17632
18054
  },
@@ -17644,11 +18066,13 @@ var NativeRenderer = class {
17644
18066
  // User drawings claim a gesture before pan when armed / over a drawing.
17645
18067
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
17646
18068
  drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
17647
- drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
18069
+ drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
18070
+ drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
18071
+ // middle-click: a selected hit takes the whole selection
17648
18072
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
17649
18073
  drawingsSnapMode: () => this.snapMode,
17650
- drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
17651
- drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
18074
+ drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
18075
+ drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
17652
18076
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
17653
18077
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
17654
18078
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -17999,8 +18423,11 @@ var NativeRenderer = class {
17999
18423
  const last = this.bars[n - 1];
18000
18424
  if (last && bar.time === last.time) {
18001
18425
  this.bars[n - 1] = bar;
18002
- if (this.liveEaseTime !== bar.time) this.syncLiveEase(bar);
18003
- this.animator.start();
18426
+ if (this.animLiveBarMs <= 0 || this.liveEaseTime !== bar.time) {
18427
+ this.syncLiveEase(bar);
18428
+ } else {
18429
+ this.animator.start();
18430
+ }
18004
18431
  } else if (!last || bar.time > last.time) {
18005
18432
  this.bars.push(bar);
18006
18433
  this.syncLiveEase(bar);
@@ -18012,6 +18439,12 @@ var NativeRenderer = class {
18012
18439
  this.scene.bars = this.bars;
18013
18440
  this.scheduler.invalidate(4 /* Full */);
18014
18441
  }
18442
+ /** Set the live-bar glide duration (0 = off). A non-zero value is also remembered as
18443
+ * what the config's on/off toggle (`priceScale.animateLastPrice`) switches back on to. */
18444
+ setLiveBarEase(ms) {
18445
+ this.animLiveBarMs = ms;
18446
+ if (ms > 0) this.animLiveBarOnMs = ms;
18447
+ }
18015
18448
  /** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
18016
18449
  syncLiveEase(bar) {
18017
18450
  this.liveEaseTime = bar.time;
@@ -18024,9 +18457,10 @@ var NativeRenderer = class {
18024
18457
  const target = this.bars[this.bars.length - 1];
18025
18458
  if (!target || this.liveEaseTime !== target.time) return false;
18026
18459
  const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
18027
- const nh = easeToward(this.liveEaseHigh, target.high, dtMs, LIVE_BAR_TAU_MS);
18028
- const nl = easeToward(this.liveEaseLow, target.low, dtMs, LIVE_BAR_TAU_MS);
18029
- const nc = easeToward(this.liveEaseClose, target.close, dtMs, LIVE_BAR_TAU_MS);
18460
+ const tau = this.animLiveBarMs;
18461
+ const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
18462
+ const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
18463
+ const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
18030
18464
  const active = Math.abs(nh - target.high) > eps || Math.abs(nl - target.low) > eps || Math.abs(nc - target.close) > eps;
18031
18465
  this.liveEaseHigh = active ? nh : target.high;
18032
18466
  this.liveEaseLow = active ? nl : target.low;
@@ -21462,7 +21896,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
21462
21896
  if (!model) continue;
21463
21897
  const paneId = model.paneId ?? "price";
21464
21898
  if (!byPane.has(paneId)) byPane.set(paneId, []);
21465
- byPane.get(paneId).push({ id: r.id, title: model.title || r.title, ownScale: model.ownScale === true });
21899
+ byPane.get(paneId).push({
21900
+ id: r.id,
21901
+ title: model.title || r.title,
21902
+ ...model.shorttitle ? { shorttitle: model.shorttitle } : {},
21903
+ ownScale: model.ownScale === true
21904
+ });
21466
21905
  }
21467
21906
  const panes = [];
21468
21907
  for (const paneId of this.paneOrder) {
@@ -22032,6 +22471,10 @@ function str3(inputs, key, fallback) {
22032
22471
  const v = inputs[key];
22033
22472
  return typeof v === "string" && v !== "" ? v : fallback;
22034
22473
  }
22474
+ function bool(inputs, key, fallback) {
22475
+ const v = inputs[key];
22476
+ return typeof v === "boolean" ? v : fallback;
22477
+ }
22035
22478
  function toPoints(bars, values, colors) {
22036
22479
  const pts = new Array(bars.length);
22037
22480
  for (let i = 0; i < bars.length; i++) {
@@ -23563,10 +24006,6 @@ var aroon = {
23563
24006
  plots: [
23564
24007
  { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
23565
24008
  { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
23566
- ],
23567
- levels: [
23568
- { key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
23569
- { key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
23570
24009
  ]
23571
24010
  };
23572
24011
  }
@@ -24064,8 +24503,10 @@ var volumeSpecs = [
24064
24503
  var DAY_MS2 = 864e5;
24065
24504
  function periodKey(anchor, time) {
24066
24505
  if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
24067
- if (anchor === "Month") {
24506
+ if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
24068
24507
  const d = new Date(time);
24508
+ if (anchor === "Year") return d.getUTCFullYear();
24509
+ if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
24069
24510
  return d.getUTCFullYear() * 12 + d.getUTCMonth();
24070
24511
  }
24071
24512
  return Math.floor(time / DAY_MS2);
@@ -24117,40 +24558,86 @@ var parabolicSar = {
24117
24558
  return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
24118
24559
  }
24119
24560
  };
24561
+ var VWAP_BANDS_GROUP = "Bands";
24562
+ var VWAP_STYLE = "Style";
24563
+ var VWAP_BANDS = [
24564
+ { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
24565
+ { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
24566
+ { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
24567
+ ];
24120
24568
  var vwap = {
24121
24569
  type: "vwap",
24122
24570
  title: "Volume Weighted Average Price",
24123
24571
  shortTitle: "VWAP",
24124
24572
  overlay: true,
24125
24573
  inputs: [
24126
- { key: "anchor", title: "Anchor", type: "string", defval: "Day", options: ["Day", "Week", "Month"], tooltip: "Where the accumulation resets (UTC periods)" },
24127
- { key: "source", title: "Source", type: "string", defval: "HLC3", options: ["HLC3", "OHLC4", "Close"] },
24128
- colorInput(INFO)
24574
+ { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
24575
+ sourceInput("HLC3"),
24576
+ // Each band is one row: its toggle leads, the multiplier follows unlabeled.
24577
+ ...VWAP_BANDS.flatMap((b, i) => [
24578
+ { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
24579
+ { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
24580
+ ]),
24581
+ { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
24582
+ // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
24583
+ ...VWAP_BANDS.flatMap((b, i) => [
24584
+ { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
24585
+ { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
24586
+ { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
24587
+ ])
24129
24588
  ],
24130
24589
  compute: (bars, inputs) => {
24131
24590
  const anchor = str3(inputs, "anchor", "Day");
24132
- const source = str3(inputs, "source", "HLC3");
24133
- const values = new Array(bars.length).fill(Number.NaN);
24591
+ const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
24592
+ const n = bars.length;
24593
+ const values = new Array(n).fill(Number.NaN);
24594
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
24595
+ mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
24596
+ color: str3(inputs, b.color, b.defColor),
24597
+ fill: bool(inputs, b.fill, true),
24598
+ fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
24599
+ up: new Array(n).fill(Number.NaN),
24600
+ down: new Array(n).fill(Number.NaN)
24601
+ }));
24134
24602
  let period = Number.NaN;
24135
24603
  let cumPV = 0;
24604
+ let cumPV2 = 0;
24136
24605
  let cumV = 0;
24137
- for (let i = 0; i < bars.length; i++) {
24606
+ for (let i = 0; i < n; i++) {
24138
24607
  const b = bars[i];
24139
24608
  const key = periodKey(anchor, b.time);
24140
24609
  if (key !== period) {
24141
24610
  period = key;
24142
24611
  cumPV = 0;
24612
+ cumPV2 = 0;
24143
24613
  cumV = 0;
24144
24614
  }
24145
24615
  const v = b.volume;
24146
24616
  if (v != null && Number.isFinite(v) && v > 0) {
24147
- const tp = source === "Close" ? b.close : source === "OHLC4" ? (b.open + b.high + b.low + b.close) / 4 : (b.high + b.low + b.close) / 3;
24617
+ const tp = src[i];
24148
24618
  cumPV += tp * v;
24619
+ cumPV2 += tp * tp * v;
24149
24620
  cumV += v;
24150
24621
  }
24151
- if (cumV > 0) values[i] = cumPV / cumV;
24152
- }
24153
- return { plots: [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }] };
24622
+ if (cumV <= 0) continue;
24623
+ const mean = cumPV / cumV;
24624
+ const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
24625
+ values[i] = mean;
24626
+ for (const band of bands) {
24627
+ band.up[i] = mean + band.mult * sd;
24628
+ band.down[i] = mean - band.mult * sd;
24629
+ }
24630
+ }
24631
+ const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
24632
+ const fills = [];
24633
+ bands.forEach((band, i) => {
24634
+ plots.push(
24635
+ { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
24636
+ { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
24637
+ );
24638
+ if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
24639
+ });
24640
+ return { plots, bands: fills };
24154
24641
  }
24155
24642
  };
24156
24643
  var pivotPoints = {
@@ -24341,21 +24828,11 @@ var Vela = class {
24341
24828
  registerClassicIndicators();
24342
24829
  const element = resolveElement(container);
24343
24830
  const theme = resolveTheme(options.theme);
24344
- let animZoom = true;
24345
- let animPan = true;
24346
- if (typeof options.animations === "boolean") {
24347
- animZoom = options.animations;
24348
- animPan = options.animations;
24349
- } else if (options.animations) {
24350
- animZoom = options.animations.zoom ?? true;
24351
- animPan = options.animations.pan ?? true;
24352
- }
24353
24831
  const display = {
24354
24832
  currentPriceLine: options.currentPriceLine ?? true,
24355
24833
  logScale: options.logScale ?? false,
24356
24834
  nativeBackend: options.nativeBackend ?? "auto",
24357
- animZoom,
24358
- animPan,
24835
+ ...resolveAnimations(options.animations),
24359
24836
  glow: options.glow ?? 0,
24360
24837
  upColor: options.upColor ?? BULLISH,
24361
24838
  downColor: options.downColor ?? BEARISH,
@@ -24670,4 +25147,4 @@ function resolveElement(container) {
24670
25147
  return element;
24671
25148
  }
24672
25149
 
24673
- export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
25150
+ export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };