@luxalgo/vela 0.6.17 → 0.6.19

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 (37) hide show
  1. package/dist/{DataProvider-B-Jz59Rf.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
  2. package/dist/{DataProvider-OfR3fXg9.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
  3. package/dist/{chunk-YCD72KGK.js → chunk-22QJKGXX.js} +565 -151
  4. package/dist/{chunk-OSZDSH7F.js → chunk-627A5HEV.js} +252 -120
  5. package/dist/{contributions-C3b0Sx5P.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
  6. package/dist/{contributions-Swzz10fG.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
  7. package/dist/index.cjs +563 -149
  8. package/dist/index.d.cts +6 -6
  9. package/dist/index.d.ts +6 -6
  10. package/dist/index.js +1 -1
  11. package/dist/{options-CX1lSYWA.d.cts → options-BCRmYALw.d.cts} +12 -3
  12. package/dist/{options-CX1lSYWA.d.ts → options-BCRmYALw.d.ts} +12 -3
  13. package/dist/{plugin-mvddudqd.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
  14. package/dist/{plugin-BjnEE1zA.d.cts → plugin-DVUaROPo.d.cts} +3 -3
  15. package/dist/plugin.d.cts +4 -4
  16. package/dist/plugin.d.ts +4 -4
  17. package/dist/providers/binance.d.cts +2 -2
  18. package/dist/providers/binance.d.ts +2 -2
  19. package/dist/providers/coinbase.d.cts +2 -2
  20. package/dist/providers/coinbase.d.ts +2 -2
  21. package/dist/providers/hyperliquid.d.cts +2 -2
  22. package/dist/providers/hyperliquid.d.ts +2 -2
  23. package/dist/{statusline-model-IURCya0H.d.ts → statusline-model-hY9cK4BS.d.cts} +17 -7
  24. package/dist/{statusline-model-D91cU2fF.d.cts → statusline-model-wW3m4in1.d.ts} +17 -7
  25. package/dist/ui.d.cts +1 -1
  26. package/dist/ui.d.ts +1 -1
  27. package/dist/vela.global.js +563 -149
  28. package/dist/vela.global.min.js +53 -51
  29. package/dist/widget.cjs +814 -268
  30. package/dist/widget.d.cts +39 -12
  31. package/dist/widget.d.ts +39 -12
  32. package/dist/widget.js +3 -3
  33. package/dist/workspace.cjs +814 -268
  34. package/dist/workspace.d.cts +14 -5
  35. package/dist/workspace.d.ts +14 -5
  36. package/dist/workspace.js +2 -2
  37. 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-IFJJPXSV.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
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, 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-CAFCLMPF.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;
@@ -5239,15 +5245,15 @@ function candleOverrideFor(style, bags) {
5239
5245
  if (!hasOwnCandlePaint(style)) return null;
5240
5246
  const bag = bags[style] ?? {};
5241
5247
  const color = (v) => isColor(v) ? v : null;
5242
- const bool = (v) => isBool(v) ? v : null;
5248
+ const bool2 = (v) => isBool(v) ? v : null;
5243
5249
  return {
5244
5250
  upColor: color(bag.candleUpColor),
5245
5251
  downColor: color(bag.candleDownColor),
5246
- bodyVisible: bool(bag.candleBodyVisible),
5247
- borderVisible: bool(bag.candleBorderVisible),
5252
+ bodyVisible: bool2(bag.candleBodyVisible),
5253
+ borderVisible: bool2(bag.candleBorderVisible),
5248
5254
  borderUpColor: color(bag.candleBorderUpColor),
5249
5255
  borderDownColor: color(bag.candleBorderDownColor),
5250
- wickVisible: bool(bag.candleWickVisible),
5256
+ wickVisible: bool2(bag.candleWickVisible),
5251
5257
  wickUpColor: color(bag.candleWickUpColor),
5252
5258
  wickDownColor: color(bag.candleWickDownColor)
5253
5259
  };
@@ -7038,7 +7044,7 @@ var InputController = class {
7038
7044
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
7039
7045
  if (Number.isNaN(this.cursorX)) return;
7040
7046
  if (this.dragging && this.region !== "drawing") return;
7041
- 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);
7042
7048
  };
7043
7049
  this.onDown = (e) => {
7044
7050
  if (e.button === 1) {
@@ -7070,7 +7076,7 @@ var InputController = class {
7070
7076
  this.startY = y;
7071
7077
  if (this.deps.drawingsClaim?.(x, y)) {
7072
7078
  this.region = "drawing";
7073
- 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);
7074
7080
  this.capture(e.pointerId);
7075
7081
  return;
7076
7082
  }
@@ -7079,6 +7085,11 @@ var InputController = class {
7079
7085
  this.capture(e.pointerId);
7080
7086
  return;
7081
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
+ }
7082
7093
  const vp = this.deps.getCoords().getViewport();
7083
7094
  this.deps.apply(vp);
7084
7095
  this.startRightOffset = vp.rightOffset;
@@ -7127,7 +7138,7 @@ var InputController = class {
7127
7138
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
7128
7139
  } else if (this.region === "drawing") {
7129
7140
  if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
7130
- 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);
7131
7142
  } else {
7132
7143
  const coords = this.deps.getCoords();
7133
7144
  const vp = coords.getViewport();
@@ -7150,7 +7161,7 @@ var InputController = class {
7150
7161
  const drawCursor = this.deps.drawingsCursor?.(x, y);
7151
7162
  const r = this.regionAt(x, y);
7152
7163
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
7153
- 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);
7154
7165
  }
7155
7166
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
7156
7167
  };
@@ -7491,12 +7502,12 @@ function defaultTradeMarkersState() {
7491
7502
  function mergeTradeMarkersState(base, patch) {
7492
7503
  const p = patch && typeof patch === "object" ? patch : {};
7493
7504
  const c = p.colors && typeof p.colors === "object" ? p.colors : {};
7494
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
7505
+ const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
7495
7506
  const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
7496
7507
  return {
7497
- visible: bool(p.visible, base.visible),
7498
- labels: bool(p.labels, base.labels),
7499
- 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),
7500
7511
  colors: {
7501
7512
  long: color(c.long, base.colors.long),
7502
7513
  short: color(c.short, base.colors.short),
@@ -11368,6 +11379,18 @@ var DrawingPainter = class {
11368
11379
  }
11369
11380
  this.paintHandles(ctx, pts);
11370
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
+ }
11371
11394
  /** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
11372
11395
  paintSnapRing(ctx, x, y, theme) {
11373
11396
  ctx.setLineDash([]);
@@ -12836,6 +12859,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
12836
12859
  }
12837
12860
  return null;
12838
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
+ }
12839
12871
 
12840
12872
  // src/renderers/native/drawings/DrawingInteraction.ts
12841
12873
  var DRAG_SLOP2 = 3;
@@ -12939,10 +12971,32 @@ var DrawingInteraction = class {
12939
12971
  activeDragId() {
12940
12972
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
12941
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
+ }
12942
12995
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
12943
12996
  * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
12944
- * while placing a line tool it locks the segment angle to 45° steps. */
12945
- 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) {
12946
13000
  const proj = this.deps.projector();
12947
13001
  if (this.state.kind === "placing") {
12948
13002
  if (this.state.draft.placementMode() !== "click") return;
@@ -12970,7 +13024,50 @@ var DrawingInteraction = class {
12970
13024
  return;
12971
13025
  }
12972
13026
  const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
12973
- 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);
12974
13071
  }
12975
13072
  }
12976
13073
  /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
@@ -12994,12 +13091,20 @@ var DrawingInteraction = class {
12994
13091
  this.deps.changed();
12995
13092
  return;
12996
13093
  }
13094
+ if (this.state.kind === "marquee") {
13095
+ this.state.x1 = x;
13096
+ this.state.y1 = y;
13097
+ this.deps.changed();
13098
+ return;
13099
+ }
12997
13100
  if (this.state.kind !== "pressed") return;
12998
13101
  const d = this.byId(this.state.id);
12999
13102
  if (!d || d.locked) return;
13000
13103
  if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
13001
13104
  this.state.moved = true;
13002
- if (this.state.handle >= 0) {
13105
+ if (this.state.handle < 0) {
13106
+ this.dragBody(d, x, y);
13107
+ } else {
13003
13108
  const a = d.anchors[this.state.handle];
13004
13109
  if (a) {
13005
13110
  const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
@@ -13007,12 +13112,6 @@ var DrawingInteraction = class {
13007
13112
  applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
13008
13113
  }
13009
13114
  d.constrainHandleDrag(this.state.handle);
13010
- } else {
13011
- const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
13012
- this.snapAt = null;
13013
- const dt = pt.time - this.state.grab.time;
13014
- const dp = pt.price - this.state.grab.price;
13015
- d.anchors = d.translateBody(dt, dp, this.state.orig);
13016
13115
  }
13017
13116
  this.deps.changed();
13018
13117
  }
@@ -13032,16 +13131,46 @@ var DrawingInteraction = class {
13032
13131
  }
13033
13132
  return;
13034
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
+ }
13035
13141
  if (this.state.kind !== "pressed") return;
13036
- const { id, moved } = this.state;
13142
+ const { id, moved, riders, mod, clones } = this.state;
13037
13143
  this.state = { kind: "idle" };
13038
13144
  const d = this.byId(id);
13039
- if (moved) {
13040
- if (d) this.deps.emit({ kind: "edit", doc: d.serialize() });
13041
- this.deps.changed();
13042
- } else {
13043
- 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;
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);
13044
13172
  }
13173
+ if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
13045
13174
  }
13046
13175
  /** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
13047
13176
  cancel() {
@@ -13054,8 +13183,15 @@ var DrawingInteraction = class {
13054
13183
  return true;
13055
13184
  }
13056
13185
  if (this.state.kind === "pressed") {
13057
- const d = this.byId(this.state.id);
13058
- 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") {
13059
13195
  this.state = { kind: "idle" };
13060
13196
  this.deps.changed();
13061
13197
  return true;
@@ -13170,6 +13306,9 @@ var DrawingInteraction = class {
13170
13306
  function cloneAnchors(d) {
13171
13307
  return d.anchors.map((a) => ({ time: a.time, price: a.price }));
13172
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
+ }
13173
13312
  function applyFree(anchor, pt, free) {
13174
13313
  if (free === "both" || free === "x") anchor.time = pt.time;
13175
13314
  if (free === "both" || free === "y") anchor.price = pt.price;
@@ -13555,10 +13694,27 @@ function btn(label, primary, onClick) {
13555
13694
  }
13556
13695
 
13557
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
+ }
13558
13714
  var BTN = 30;
13559
13715
  var ICON = 21;
13560
13716
  var STYLE_ID4 = "vela-drawing-popup-styles";
13561
- var STYLE_REV = "3";
13717
+ var STYLE_REV = "4";
13562
13718
  function ensureStyles4() {
13563
13719
  if (typeof document === "undefined") return;
13564
13720
  const existing = document.getElementById(STYLE_ID4);
@@ -13570,6 +13726,8 @@ function ensureStyles4() {
13570
13726
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
13571
13727
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
13572
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;}
13573
13731
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
13574
13732
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
13575
13733
  .vela-dpop-item[data-active='1']{background:var(--vela-active);}
@@ -13601,7 +13759,7 @@ var DrawingSettingsPopup = class {
13601
13759
  if (this.isOwnChrome(node)) return;
13602
13760
  const cb = this.onClose;
13603
13761
  this.close();
13604
- cb?.();
13762
+ cb?.(e);
13605
13763
  };
13606
13764
  this.theme = theme;
13607
13765
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
@@ -13626,16 +13784,27 @@ var DrawingSettingsPopup = class {
13626
13784
  contains(node) {
13627
13785
  return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
13628
13786
  }
13629
- /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
13630
- * `onClose` fires on an outside-click dismissal (not a programmatic close). */
13631
- 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) {
13632
13795
  this.close();
13796
+ const drawing = drawings[0];
13797
+ if (!drawing) return;
13633
13798
  ensureStyles4();
13634
13799
  this.onClose = onClose ?? null;
13635
13800
  const t = this.theme;
13801
+ const multi = drawings.length > 1;
13636
13802
  const schema = drawing.schema();
13637
- const paths = new Set(schema.fields.map((f) => f.path));
13638
- 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");
13639
13808
  const el = document.createElement("div");
13640
13809
  el.className = "vela-dpop";
13641
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;`;
@@ -13649,75 +13818,84 @@ var DrawingSettingsPopup = class {
13649
13818
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
13650
13819
  bar.appendChild(this.dragHandle());
13651
13820
  if (paths.has("glyph")) {
13652
- const cur = drawing.glyph ?? GLYPH_OPTIONS[0];
13821
+ const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
13653
13822
  bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
13654
13823
  }
13655
13824
  if (paths.has("size")) {
13656
- const sz = drawing.size ?? "normal";
13825
+ const sz = common((d) => d.size ?? "normal");
13657
13826
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
13658
13827
  }
13659
- if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
13828
+ if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
13829
+ const mag = (d) => d;
13660
13830
  const options = this.lowerTimeframeOptions();
13661
13831
  if (options.length > 0) {
13662
13832
  bar.appendChild(
13663
- 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 }), {
13664
13834
  label: (v) => magnifierTimeframeLabel(String(v)),
13665
13835
  labelInTrigger: true
13666
13836
  })
13667
13837
  );
13668
13838
  }
13669
- bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
13670
- 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"));
13671
13841
  }
13672
- 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"));
13673
13843
  if (paths.has("style.lineWidth")) {
13674
- const wf = schema.fields.find((f) => f.path === "style.lineWidth");
13675
- if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
13676
- 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 })));
13677
13853
  } else {
13678
- 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 }));
13679
13855
  }
13680
13856
  }
13681
- 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 }));
13682
- 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"));
13683
13859
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
13684
- const isPosition = paths.has("riskPercent") && drawing instanceof PositionTool;
13860
+ const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
13685
13861
  if (isPosition) {
13686
- const pos = drawing;
13687
- bar.appendChild(this.colorButton("Profit zone", BUCKET_ICON, pos.profitColor, (v) => actions.patch({ profitColor: v })));
13688
- 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"));
13689
13865
  }
13690
- const reg = drawing.reg;
13691
- 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);
13692
13869
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
13693
- bar.appendChild(this.colorButton("Midline color", BRUSH_ICON, reg.midColor, (v) => actions.patch({ "reg.midColor": v })));
13694
- bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
13695
- bar.appendChild(this.colorButton("Upper line color", BRUSH_ICON, reg.upperColor, (v) => actions.patch({ "reg.upperColor": v })));
13696
- bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
13697
- bar.appendChild(this.colorButton("Lower line color", BRUSH_ICON, reg.lowerColor, (v) => actions.patch({ "reg.lowerColor": v })));
13698
- bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
13699
- bar.appendChild(this.colorButton("Upper fill", BUCKET_ICON, reg.upperFill, (v) => actions.patch({ "reg.upperFill": v })));
13700
- bar.appendChild(this.colorButton("Lower fill", BUCKET_ICON, reg.lowerFill, (v) => actions.patch({ "reg.lowerFill": v })));
13701
- bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
13702
- }
13703
- const vwap2 = drawing.vwap;
13704
- 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);
13705
13883
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
13706
13884
  const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
13707
- bar.appendChild(this.colorButton("VWAP color", BRUSH_ICON, vwap2.midColor, (v) => actions.patch({ "vwap.midColor": v })));
13708
- 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 }));
13709
13887
  bar.appendChild(
13710
- 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 }), {
13711
13889
  label: (v) => `${v}\u03C3`,
13712
13890
  labelInTrigger: true
13713
13891
  })
13714
13892
  );
13715
- bar.appendChild(this.colorButton("Upper band color", BRUSH_ICON, vwap2.upperColor, (v) => actions.patch({ "vwap.upperColor": v })));
13716
- bar.appendChild(this.colorButton("Lower band color", BRUSH_ICON, vwap2.lowerColor, (v) => actions.patch({ "vwap.lowerColor": v })));
13717
- 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"));
13718
13896
  }
13719
13897
  if (paths.has("maxCurvature")) {
13720
- const cur = drawing.maxCurvature ?? 24;
13898
+ const cur = common((d) => d.maxCurvature ?? 24);
13721
13899
  bar.appendChild(
13722
13900
  this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON, (v) => actions.patch({ maxCurvature: v }), {
13723
13901
  label: (v) => `n=${v}`,
@@ -13726,7 +13904,7 @@ var DrawingSettingsPopup = class {
13726
13904
  );
13727
13905
  }
13728
13906
  if (paths.has("mach")) {
13729
- const cur = drawing.mach ?? 2;
13907
+ const cur = common((d) => d.mach ?? 2);
13730
13908
  bar.appendChild(
13731
13909
  this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON, (v) => actions.patch({ mach: v }), {
13732
13910
  label: (v) => `M=${v}`,
@@ -13735,7 +13913,7 @@ var DrawingSettingsPopup = class {
13735
13913
  );
13736
13914
  }
13737
13915
  if (paths.has("waveCount")) {
13738
- const cur = drawing.waveCount ?? 6;
13916
+ const cur = common((d) => d.waveCount ?? 6);
13739
13917
  bar.appendChild(
13740
13918
  this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON, (v) => actions.patch({ waveCount: v }), {
13741
13919
  label: (v) => `${v}`,
@@ -13743,26 +13921,27 @@ var DrawingSettingsPopup = class {
13743
13921
  })
13744
13922
  );
13745
13923
  }
13746
- const toggles = drawing;
13747
- if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, toggles.showPrice !== false, (v) => actions.patch({ showPrice: v })));
13748
- if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, toggles.showDate !== false, (v) => actions.patch({ showDate: v })));
13749
- 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 })));
13750
- 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 }));
13751
- 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 })));
13752
- 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 })));
13753
- 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)));
13754
13933
  const editableLevels = drawing.editableLevels();
13755
- if (editableLevels && !(drawing instanceof MachFigure)) {
13756
- const fib = drawing;
13934
+ if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
13935
+ const fib = (d) => d;
13757
13936
  const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
13758
- bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
13759
- 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 }));
13760
13939
  }
13761
13940
  bar.appendChild(this.divider());
13762
- if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
13763
- if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
13764
- if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
13765
- 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));
13766
13945
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
13767
13946
  del.style.color = "var(--vela-danger)";
13768
13947
  bar.appendChild(del);
@@ -13948,7 +14127,7 @@ var DrawingSettingsPopup = class {
13948
14127
  });
13949
14128
  return b;
13950
14129
  }
13951
- /** The trailing kebab overflow — z-order actions + reset settings. */
14130
+ /** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
13952
14131
  kebabButton(actions) {
13953
14132
  const b = this.base("More");
13954
14133
  b.innerHTML = sized(KEBAB_ICON);
@@ -13960,6 +14139,7 @@ var DrawingSettingsPopup = class {
13960
14139
  return;
13961
14140
  }
13962
14141
  this.openActionMenu(b, [
14142
+ { icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
13963
14143
  { icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
13964
14144
  { icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
13965
14145
  { icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
@@ -14116,26 +14296,30 @@ var DrawingSettingsPopup = class {
14116
14296
  this.tipEl?.remove();
14117
14297
  this.tipEl = null;
14118
14298
  }
14119
- 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) {
14120
14303
  const b = this.base(tip);
14121
- b.innerHTML = sized(icon2);
14122
14304
  const set = (on2) => {
14123
- 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);
14124
14307
  };
14125
14308
  set(active);
14126
14309
  let on = active;
14127
14310
  b.addEventListener("click", () => {
14128
- on = !on;
14311
+ on = on === MIXED ? true : !on;
14129
14312
  set(on);
14130
14313
  onChange(on);
14131
14314
  });
14132
14315
  return b;
14133
14316
  }
14134
14317
  /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
14135
- * 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. */
14136
14320
  widthInput(tip, value, min, max, step, onChange) {
14137
14321
  const ni = new NumberInput({
14138
- value,
14322
+ value: value === MIXED ? min : value,
14139
14323
  min,
14140
14324
  max,
14141
14325
  step,
@@ -14147,13 +14331,18 @@ var DrawingSettingsPopup = class {
14147
14331
  title: tip,
14148
14332
  onChange
14149
14333
  });
14334
+ if (value === MIXED) {
14335
+ ni.input.value = "";
14336
+ ni.input.placeholder = "\u2014";
14337
+ }
14150
14338
  ni.el.dataset.tip = tip;
14151
14339
  trapChartKeys(ni.el);
14152
14340
  return ni.el;
14153
14341
  }
14154
14342
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
14155
14343
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
14156
- * 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. */
14157
14346
  dropdown(tip, values, current, render, onChange, opts = {}) {
14158
14347
  const b = this.base(tip);
14159
14348
  b.style.width = "auto";
@@ -14163,14 +14352,20 @@ var DrawingSettingsPopup = class {
14163
14352
  let cur = current;
14164
14353
  const paint = (v) => {
14165
14354
  b.replaceChildren();
14166
- 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);
14167
14362
  if (glyph) {
14168
14363
  const ic = document.createElement("span");
14169
14364
  ic.style.cssText = "display:flex;";
14170
14365
  ic.innerHTML = sized(glyph);
14171
14366
  b.appendChild(ic);
14172
14367
  }
14173
- if (opts.label && opts.labelInTrigger) {
14368
+ if (opts.label && opts.labelInTrigger && v !== MIXED) {
14174
14369
  const tx = document.createElement("span");
14175
14370
  tx.textContent = opts.label(v);
14176
14371
  tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
@@ -14240,8 +14435,10 @@ var DrawingSettingsPopup = class {
14240
14435
  }
14241
14436
  /** A color control: an `icon` over a thin **colored underline** showing the current
14242
14437
  * value (the common idiom), with an invisible native picker overlaid to edit it.
14243
- * `iconSize` shrinks it for the floating text controls. */
14244
- 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 = []) {
14245
14442
  const b = document.createElement("button");
14246
14443
  b.type = "button";
14247
14444
  b.dataset.tip = tip;
@@ -14251,13 +14448,14 @@ var DrawingSettingsPopup = class {
14251
14448
  ic.style.cssText = "display:flex;";
14252
14449
  ic.innerHTML = sized(icon2, iconSize);
14253
14450
  const bar = document.createElement("span");
14254
- 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};`;
14255
14452
  b.append(ic, bar);
14256
14453
  let cur = color;
14257
14454
  b.addEventListener("pointerdown", (e) => e.stopPropagation());
14258
14455
  b.addEventListener("click", (e) => {
14259
14456
  e.stopPropagation();
14260
- 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) => {
14261
14459
  cur = v;
14262
14460
  bar.style.background = v;
14263
14461
  onChange(v);
@@ -14266,20 +14464,42 @@ var DrawingSettingsPopup = class {
14266
14464
  return b;
14267
14465
  }
14268
14466
  /** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
14269
- toggleColorPopover(anchor, color, onChange) {
14467
+ toggleColorPopover(anchor, color, used, onChange) {
14270
14468
  if (this.colorOwner === anchor) {
14271
14469
  this.closeColorPopover();
14272
14470
  return;
14273
14471
  }
14274
- this.openColorPopover(anchor, color, onChange);
14472
+ this.openColorPopover(anchor, color, used, onChange);
14275
14473
  }
14276
- /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
14277
- 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) {
14278
14477
  const t = this.theme;
14279
14478
  this.colorPop = this.hostFloat(anchor, {
14280
14479
  zIndex: 25,
14281
14480
  padding: "10px",
14282
- 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
+ }
14283
14503
  el.appendChild(buildColorPicker(color, t, onChange));
14284
14504
  }
14285
14505
  });
@@ -14300,6 +14520,7 @@ var TYPE_ICON = icon("type");
14300
14520
  var PRICE_DELTA_ICON = icon("price-delta");
14301
14521
  var DATE_DELTA_ICON = icon("date-delta");
14302
14522
  var LOCK_ICON = icon("lock");
14523
+ var UNLOCK_ICON = icon("unlock");
14303
14524
  var FRONT_ICON = icon("bring-front");
14304
14525
  var BACK_ICON = icon("send-back");
14305
14526
  var TRASH_ICON = icon("trash");
@@ -14307,6 +14528,7 @@ var BOLD_ICON = icon("bold");
14307
14528
  var ITALIC_ICON = icon("italic");
14308
14529
  var GRIP_ICON = icon("grip");
14309
14530
  var KEBAB_ICON = icon("kebab");
14531
+ var CLONE_ICON = icon("clone");
14310
14532
  var RESET_ICON = icon("reset");
14311
14533
  var GEAR_ICON = icon("gear");
14312
14534
  var CHEVRON_ICON = icon("chevron-down");
@@ -14315,6 +14537,12 @@ var BANDS_ICON = icon("bands");
14315
14537
  var DEDEKIND_ICON = icon("dedekind");
14316
14538
  var SONIC_ICON = icon("sonic");
14317
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
+ }
14318
14546
  function lineIcon(width, style) {
14319
14547
  const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
14320
14548
  return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
@@ -14598,6 +14826,20 @@ var UserDrawingController = class {
14598
14826
  // reported exactly when it actually changes).
14599
14827
  this.textEditor = null;
14600
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
+ };
14601
14843
  this.ctx = canvas.getContext("2d");
14602
14844
  this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
14603
14845
  this.toolbar = new DrawingToolbar(
@@ -14774,6 +15016,12 @@ var UserDrawingController = class {
14774
15016
  }
14775
15017
  setSelection(ids) {
14776
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
+ }
14777
15025
  this.render();
14778
15026
  }
14779
15027
  /** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
@@ -14819,12 +15067,16 @@ var UserDrawingController = class {
14819
15067
  });
14820
15068
  }
14821
15069
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
14822
- * Shared by the eraser (click + drag) and the middle-click shortcut. */
14823
- 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) {
14824
15074
  const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
14825
- 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;
14826
15078
  this.popup.close();
14827
- this.emit({ kind: "delete", ids: [hit.id] });
15079
+ this.emit({ kind: "delete", ids });
14828
15080
  return true;
14829
15081
  }
14830
15082
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
@@ -14850,7 +15102,14 @@ var UserDrawingController = class {
14850
15102
  this.render();
14851
15103
  }
14852
15104
  }
14853
- 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) {
14854
15113
  if (this.eraserMode) {
14855
15114
  this.erasing = true;
14856
15115
  this.deleteAt(x, y);
@@ -14870,9 +15129,9 @@ var UserDrawingController = class {
14870
15129
  return;
14871
15130
  }
14872
15131
  }
14873
- this.interaction.down(x, y, snap, shift2);
15132
+ this.interaction.down(x, y, snap, shift2, mod);
14874
15133
  }
14875
- pointerMove(x, y, snap = "off", shift2 = false) {
15134
+ pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
14876
15135
  if (this.eraserMode) {
14877
15136
  if (this.erasing) this.deleteAt(x, y);
14878
15137
  return;
@@ -14884,12 +15143,14 @@ var UserDrawingController = class {
14884
15143
  return;
14885
15144
  }
14886
15145
  this.interaction.move(x, y, snap, shift2);
14887
- this.updateHover(x, y);
15146
+ this.updateHover(x, y, mod);
14888
15147
  }
14889
- /** Track which drawing is hovered so its handles appear on hover (and only then). */
14890
- 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) {
14891
15152
  let id = null;
14892
- if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
15153
+ if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
14893
15154
  id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
14894
15155
  }
14895
15156
  if (id !== this.hoveredId) {
@@ -14909,7 +15170,12 @@ var UserDrawingController = class {
14909
15170
  this.render();
14910
15171
  return;
14911
15172
  }
15173
+ const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
14912
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
+ }
14913
15179
  }
14914
15180
  /** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
14915
15181
  toggleMeasure() {
@@ -15174,7 +15440,7 @@ var UserDrawingController = class {
15174
15440
  this.emit({ kind: "duplicate", ids: this.selectionIds() });
15175
15441
  break;
15176
15442
  case "delete": {
15177
- const ids = this.selectedIds.size ? this.selectionIds() : this.hoveredId ? [this.hoveredId] : [];
15443
+ const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
15178
15444
  if (ids.length) {
15179
15445
  this.popup.close();
15180
15446
  this.emit({ kind: "delete", ids });
@@ -15190,6 +15456,9 @@ var UserDrawingController = class {
15190
15456
  selectionIds() {
15191
15457
  return [...this.selectedIds];
15192
15458
  }
15459
+ deletableSelection() {
15460
+ return deletableSelection(this.selectedIds, this.drawings);
15461
+ }
15193
15462
  /** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
15194
15463
  nudgeSelection(dx, dy) {
15195
15464
  const proj = this.deps.projector();
@@ -15298,6 +15567,8 @@ var UserDrawingController = class {
15298
15567
  this.painter.seriesLook = this.deps.seriesLook();
15299
15568
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
15300
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)) });
15301
15572
  this.layoutTextEditor();
15302
15573
  const ghost = this.interaction.ghost();
15303
15574
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
@@ -15313,6 +15584,8 @@ var UserDrawingController = class {
15313
15584
  const my = m ? proj.yOf(m.point.price, m.paneId) : null;
15314
15585
  if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
15315
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);
15316
15589
  }
15317
15590
  destroy() {
15318
15591
  this.closeTextEditor();
@@ -15330,15 +15603,80 @@ var UserDrawingController = class {
15330
15603
  if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
15331
15604
  this.render();
15332
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
+ }
15333
15670
  /** A click on a drawing opens its settings toolbar (text labels edit text here too). */
15334
15671
  openSettingsById(id, _x, _y) {
15335
15672
  const drawing = this.drawings.find((d) => d.id === id);
15336
15673
  if (!drawing) return;
15337
15674
  const live = () => this.drawings.find((d) => d.id === id);
15675
+ this.popupMulti = false;
15338
15676
  this.emit({ kind: "select", ids: [id] });
15339
15677
  this.emit({ kind: "settings", id });
15340
15678
  const anchor = drawing.bounds(this.deps.projector());
15341
- this.popup.open(drawing, anchor, {
15679
+ this.popup.open([drawing], anchor, {
15342
15680
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
15343
15681
  // position tool's price fields, where one edit can flip another level) resolves fresh.
15344
15682
  resolve: () => live() ?? null,
@@ -15356,6 +15694,11 @@ var UserDrawingController = class {
15356
15694
  this.emit({ kind: "edit", doc: d.serialize() });
15357
15695
  },
15358
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
+ },
15359
15702
  resetSettings: () => {
15360
15703
  const d = live();
15361
15704
  if (!d) return;
@@ -15378,7 +15721,13 @@ var UserDrawingController = class {
15378
15721
  this.popup.close();
15379
15722
  this.emit({ kind: "delete", ids: [id] });
15380
15723
  }
15381
- }, () => 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();
15382
15731
  }
15383
15732
  /** Report placement progress upstream (`draft` intent) so the drawings sync can
15384
15733
  * mirror the ghost live on linked charts. Called from every render; only actual
@@ -15404,6 +15753,13 @@ var UserDrawingController = class {
15404
15753
  }, 0);
15405
15754
  return;
15406
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
+ }
15407
15763
  this.intentCb?.(i);
15408
15764
  }
15409
15765
  };
@@ -17689,7 +18045,10 @@ var NativeRenderer = class {
17689
18045
  zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
17690
18046
  fling: (v) => this.fling(v),
17691
18047
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
17692
- onClick: (x) => this.handleClick(x),
18048
+ onClick: (x) => {
18049
+ this.userDrawings?.deselect();
18050
+ this.handleClick(x);
18051
+ },
17693
18052
  onAxisLongPress: (axis, x, y) => {
17694
18053
  for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
17695
18054
  },
@@ -17707,11 +18066,13 @@ var NativeRenderer = class {
17707
18066
  // User drawings claim a gesture before pan when armed / over a drawing.
17708
18067
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
17709
18068
  drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
17710
- 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
17711
18072
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
17712
18073
  drawingsSnapMode: () => this.snapMode,
17713
- drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
17714
- 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),
17715
18076
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
17716
18077
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
17717
18078
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -21535,7 +21896,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
21535
21896
  if (!model) continue;
21536
21897
  const paneId = model.paneId ?? "price";
21537
21898
  if (!byPane.has(paneId)) byPane.set(paneId, []);
21538
- 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
+ });
21539
21905
  }
21540
21906
  const panes = [];
21541
21907
  for (const paneId of this.paneOrder) {
@@ -22105,6 +22471,10 @@ function str3(inputs, key, fallback) {
22105
22471
  const v = inputs[key];
22106
22472
  return typeof v === "string" && v !== "" ? v : fallback;
22107
22473
  }
22474
+ function bool(inputs, key, fallback) {
22475
+ const v = inputs[key];
22476
+ return typeof v === "boolean" ? v : fallback;
22477
+ }
22108
22478
  function toPoints(bars, values, colors) {
22109
22479
  const pts = new Array(bars.length);
22110
22480
  for (let i = 0; i < bars.length; i++) {
@@ -23636,10 +24006,6 @@ var aroon = {
23636
24006
  plots: [
23637
24007
  { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
23638
24008
  { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
23639
- ],
23640
- levels: [
23641
- { key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
23642
- { key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
23643
24009
  ]
23644
24010
  };
23645
24011
  }
@@ -24137,8 +24503,10 @@ var volumeSpecs = [
24137
24503
  var DAY_MS2 = 864e5;
24138
24504
  function periodKey(anchor, time) {
24139
24505
  if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
24140
- if (anchor === "Month") {
24506
+ if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
24141
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);
24142
24510
  return d.getUTCFullYear() * 12 + d.getUTCMonth();
24143
24511
  }
24144
24512
  return Math.floor(time / DAY_MS2);
@@ -24190,40 +24558,86 @@ var parabolicSar = {
24190
24558
  return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
24191
24559
  }
24192
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
+ ];
24193
24568
  var vwap = {
24194
24569
  type: "vwap",
24195
24570
  title: "Volume Weighted Average Price",
24196
24571
  shortTitle: "VWAP",
24197
24572
  overlay: true,
24198
24573
  inputs: [
24199
- { key: "anchor", title: "Anchor", type: "string", defval: "Day", options: ["Day", "Week", "Month"], tooltip: "Where the accumulation resets (UTC periods)" },
24200
- { key: "source", title: "Source", type: "string", defval: "HLC3", options: ["HLC3", "OHLC4", "Close"] },
24201
- 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
+ ])
24202
24588
  ],
24203
24589
  compute: (bars, inputs) => {
24204
24590
  const anchor = str3(inputs, "anchor", "Day");
24205
- const source = str3(inputs, "source", "HLC3");
24206
- 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
+ }));
24207
24602
  let period = Number.NaN;
24208
24603
  let cumPV = 0;
24604
+ let cumPV2 = 0;
24209
24605
  let cumV = 0;
24210
- for (let i = 0; i < bars.length; i++) {
24606
+ for (let i = 0; i < n; i++) {
24211
24607
  const b = bars[i];
24212
24608
  const key = periodKey(anchor, b.time);
24213
24609
  if (key !== period) {
24214
24610
  period = key;
24215
24611
  cumPV = 0;
24612
+ cumPV2 = 0;
24216
24613
  cumV = 0;
24217
24614
  }
24218
24615
  const v = b.volume;
24219
24616
  if (v != null && Number.isFinite(v) && v > 0) {
24220
- 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];
24221
24618
  cumPV += tp * v;
24619
+ cumPV2 += tp * tp * v;
24222
24620
  cumV += v;
24223
24621
  }
24224
- if (cumV > 0) values[i] = cumPV / cumV;
24225
- }
24226
- 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 };
24227
24641
  }
24228
24642
  };
24229
24643
  var pivotPoints = {