@luxalgo/vela 0.6.17 → 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 (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-BXF7VNNE.js} +177 -104
  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-D91cU2fF.d.cts → statusline-model-7ECSDtgt.d.cts} +3 -3
  24. package/dist/{statusline-model-IURCya0H.d.ts → statusline-model-D1Fhs8Dk.d.ts} +3 -3
  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 +739 -252
  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 +739 -252
  34. package/dist/workspace.d.cts +5 -5
  35. package/dist/workspace.d.ts +5 -5
  36. package/dist/workspace.js +2 -2
  37. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -6924,15 +6924,18 @@ var DrawingController = class {
6924
6924
  next = [...ids];
6925
6925
  }
6926
6926
  this.selectedIds = next.filter((id) => this.store.has(id));
6927
+ this.announceSelection();
6928
+ }
6929
+ /** Push the selection to the renderer and announce it (primary + the full list). */
6930
+ announceSelection() {
6927
6931
  this.port?.setSelection(this.selectedIds);
6928
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
6932
+ this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
6929
6933
  }
6930
6934
  /** Restore a history snapshot, reconciling selection against what survived. */
6931
6935
  restoreSnapshot(doc) {
6932
6936
  this.store.load(doc);
6933
6937
  this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
6934
- this.port?.setSelection(this.selectedIds);
6935
- this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
6938
+ this.announceSelection();
6936
6939
  }
6937
6940
  /** Delete drawings as one undo step; prune them from the selection. */
6938
6941
  deleteMany(ids) {
@@ -7065,6 +7068,9 @@ var DrawingController = class {
7065
7068
  case "duplicate":
7066
7069
  this.duplicate(i.ids);
7067
7070
  break;
7071
+ case "clone":
7072
+ if (this.enabled && i.docs.length) this.insertClones(i.docs);
7073
+ break;
7068
7074
  case "copy":
7069
7075
  this.copy(i.ids);
7070
7076
  break;
@@ -8901,7 +8907,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
8901
8907
  if (!model) continue;
8902
8908
  const paneId = model.paneId ?? "price";
8903
8909
  if (!byPane.has(paneId)) byPane.set(paneId, []);
8904
- byPane.get(paneId).push({ id: r.id, title: model.title || r.title, ownScale: model.ownScale === true });
8910
+ byPane.get(paneId).push({
8911
+ id: r.id,
8912
+ title: model.title || r.title,
8913
+ ...model.shorttitle ? { shorttitle: model.shorttitle } : {},
8914
+ ownScale: model.ownScale === true
8915
+ });
8905
8916
  }
8906
8917
  const panes = [];
8907
8918
  for (const paneId of this.paneOrder) {
@@ -15532,15 +15543,15 @@ function candleOverrideFor(style, bags) {
15532
15543
  if (!hasOwnCandlePaint(style)) return null;
15533
15544
  const bag = bags[style] ?? {};
15534
15545
  const color = (v) => isColor(v) ? v : null;
15535
- const bool = (v) => isBool(v) ? v : null;
15546
+ const bool2 = (v) => isBool(v) ? v : null;
15536
15547
  return {
15537
15548
  upColor: color(bag.candleUpColor),
15538
15549
  downColor: color(bag.candleDownColor),
15539
- bodyVisible: bool(bag.candleBodyVisible),
15540
- borderVisible: bool(bag.candleBorderVisible),
15550
+ bodyVisible: bool2(bag.candleBodyVisible),
15551
+ borderVisible: bool2(bag.candleBorderVisible),
15541
15552
  borderUpColor: color(bag.candleBorderUpColor),
15542
15553
  borderDownColor: color(bag.candleBorderDownColor),
15543
- wickVisible: bool(bag.candleWickVisible),
15554
+ wickVisible: bool2(bag.candleWickVisible),
15544
15555
  wickUpColor: color(bag.candleWickUpColor),
15545
15556
  wickDownColor: color(bag.candleWickDownColor)
15546
15557
  };
@@ -17331,7 +17342,7 @@ var InputController = class {
17331
17342
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
17332
17343
  if (Number.isNaN(this.cursorX)) return;
17333
17344
  if (this.dragging && this.region !== "drawing") return;
17334
- this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
17345
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
17335
17346
  };
17336
17347
  this.onDown = (e) => {
17337
17348
  if (e.button === 1) {
@@ -17363,7 +17374,7 @@ var InputController = class {
17363
17374
  this.startY = y;
17364
17375
  if (this.deps.drawingsClaim?.(x, y)) {
17365
17376
  this.region = "drawing";
17366
- this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
17377
+ this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
17367
17378
  this.capture(e.pointerId);
17368
17379
  return;
17369
17380
  }
@@ -17372,6 +17383,11 @@ var InputController = class {
17372
17383
  this.capture(e.pointerId);
17373
17384
  return;
17374
17385
  }
17386
+ if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
17387
+ this.region = "drawing";
17388
+ this.capture(e.pointerId);
17389
+ return;
17390
+ }
17375
17391
  const vp = this.deps.getCoords().getViewport();
17376
17392
  this.deps.apply(vp);
17377
17393
  this.startRightOffset = vp.rightOffset;
@@ -17420,7 +17436,7 @@ var InputController = class {
17420
17436
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
17421
17437
  } else if (this.region === "drawing") {
17422
17438
  if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
17423
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
17439
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
17424
17440
  } else {
17425
17441
  const coords = this.deps.getCoords();
17426
17442
  const vp = coords.getViewport();
@@ -17443,7 +17459,7 @@ var InputController = class {
17443
17459
  const drawCursor = this.deps.drawingsCursor?.(x, y);
17444
17460
  const r = this.regionAt(x, y);
17445
17461
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
17446
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
17462
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
17447
17463
  }
17448
17464
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
17449
17465
  };
@@ -17784,12 +17800,12 @@ function defaultTradeMarkersState() {
17784
17800
  function mergeTradeMarkersState(base, patch) {
17785
17801
  const p = patch && typeof patch === "object" ? patch : {};
17786
17802
  const c = p.colors && typeof p.colors === "object" ? p.colors : {};
17787
- const bool = (v, fb) => typeof v === "boolean" ? v : fb;
17803
+ const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
17788
17804
  const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
17789
17805
  return {
17790
- visible: bool(p.visible, base.visible),
17791
- labels: bool(p.labels, base.labels),
17792
- qty: bool(p.qty, base.qty),
17806
+ visible: bool2(p.visible, base.visible),
17807
+ labels: bool2(p.labels, base.labels),
17808
+ qty: bool2(p.qty, base.qty),
17793
17809
  colors: {
17794
17810
  long: color(c.long, base.colors.long),
17795
17811
  short: color(c.short, base.colors.short),
@@ -21739,6 +21755,18 @@ var DrawingPainter = class {
21739
21755
  }
21740
21756
  this.paintHandles(ctx, pts);
21741
21757
  }
21758
+ /** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
21759
+ * accent under a thin dashed outline. */
21760
+ paintMarquee(ctx, rect) {
21761
+ ctx.save();
21762
+ ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
21763
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
21764
+ ctx.strokeStyle = HANDLE_BORDER;
21765
+ ctx.lineWidth = 1;
21766
+ ctx.setLineDash([4, 3]);
21767
+ ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
21768
+ ctx.restore();
21769
+ }
21742
21770
  /** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
21743
21771
  paintSnapRing(ctx, x, y, theme) {
21744
21772
  ctx.setLineDash([]);
@@ -23207,6 +23235,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
23207
23235
  }
23208
23236
  return null;
23209
23237
  }
23238
+ function deletableSelection(selected, drawings) {
23239
+ const ids = [...selected];
23240
+ if (ids.length < 2) return ids;
23241
+ return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
23242
+ }
23243
+ function deleteTargets(hit, selected, drawings, withSelection) {
23244
+ if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
23245
+ return hit.locked ? [] : [hit.id];
23246
+ }
23210
23247
 
23211
23248
  // src/renderers/native/drawings/DrawingInteraction.ts
23212
23249
  var DRAG_SLOP2 = 3;
@@ -23310,10 +23347,32 @@ var DrawingInteraction = class {
23310
23347
  activeDragId() {
23311
23348
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
23312
23349
  }
23350
+ /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
23351
+ pressedId() {
23352
+ return this.state.kind === "pressed" ? this.state.id : null;
23353
+ }
23354
+ /** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
23355
+ dragClones() {
23356
+ return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
23357
+ }
23358
+ /** The marquee box being swept, normalized (or null when none is in progress). */
23359
+ marqueeRect() {
23360
+ if (this.state.kind !== "marquee") return null;
23361
+ const { x0, y0, x1, y1 } = this.state;
23362
+ return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
23363
+ }
23364
+ /** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
23365
+ * armed or a gesture is in flight (that path owns the press). */
23366
+ beginMarquee(x, y) {
23367
+ if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
23368
+ this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
23369
+ return true;
23370
+ }
23313
23371
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
23314
23372
  * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
23315
- * while placing a line tool it locks the segment angle to 45° steps. */
23316
- down(x, y, mode = "off", shift2 = false) {
23373
+ * while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
23374
+ * drawing also toggles on a click, but a body drag then moves a COPY. */
23375
+ down(x, y, mode = "off", shift2 = false, mod = false) {
23317
23376
  const proj = this.deps.projector();
23318
23377
  if (this.state.kind === "placing") {
23319
23378
  if (this.state.draft.placementMode() !== "click") return;
@@ -23341,7 +23400,50 @@ var DrawingInteraction = class {
23341
23400
  return;
23342
23401
  }
23343
23402
  const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
23344
- this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
23403
+ const riders = [];
23404
+ if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
23405
+ for (const id of this.deps.selectedIds()) {
23406
+ const d = id === hit.id ? null : this.byId(id);
23407
+ if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
23408
+ }
23409
+ }
23410
+ this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
23411
+ }
23412
+ }
23413
+ /** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
23414
+ * pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
23415
+ bodyDelta(paneId, x, y) {
23416
+ if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
23417
+ const proj = this.deps.projector();
23418
+ const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
23419
+ const to = proj.pxToPoint(x, y, paneId);
23420
+ return { dt: to.time - from.time, dp: to.price - from.price };
23421
+ }
23422
+ /** The pressed drawing and its riders, each with its anchors at the press. */
23423
+ dragSources(d) {
23424
+ if (this.state.kind !== "pressed") return [];
23425
+ const out = [{ d, orig: this.state.orig }];
23426
+ for (const r of this.state.riders) {
23427
+ const rd = this.byId(r.id);
23428
+ if (rd) out.push({ d: rd, orig: r.orig });
23429
+ }
23430
+ return out;
23431
+ }
23432
+ /** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
23433
+ * cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
23434
+ * sources that have not moved yet, so their own anchors are the origin to translate from. */
23435
+ dragBody(d, x, y) {
23436
+ if (this.state.kind !== "pressed") return;
23437
+ this.snapAt = null;
23438
+ if (this.state.mod && !this.state.clones) {
23439
+ this.state.clones = this.dragSources(d).flatMap((s) => {
23440
+ const c = deserializeDrawing(s.d.serialize());
23441
+ return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
23442
+ });
23443
+ }
23444
+ for (const t of this.state.clones ?? this.dragSources(d)) {
23445
+ const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
23446
+ t.d.anchors = t.d.translateBody(dt, dp, t.orig);
23345
23447
  }
23346
23448
  }
23347
23449
  /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
@@ -23365,12 +23467,20 @@ var DrawingInteraction = class {
23365
23467
  this.deps.changed();
23366
23468
  return;
23367
23469
  }
23470
+ if (this.state.kind === "marquee") {
23471
+ this.state.x1 = x;
23472
+ this.state.y1 = y;
23473
+ this.deps.changed();
23474
+ return;
23475
+ }
23368
23476
  if (this.state.kind !== "pressed") return;
23369
23477
  const d = this.byId(this.state.id);
23370
23478
  if (!d || d.locked) return;
23371
23479
  if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
23372
23480
  this.state.moved = true;
23373
- if (this.state.handle >= 0) {
23481
+ if (this.state.handle < 0) {
23482
+ this.dragBody(d, x, y);
23483
+ } else {
23374
23484
  const a = d.anchors[this.state.handle];
23375
23485
  if (a) {
23376
23486
  const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
@@ -23378,12 +23488,6 @@ var DrawingInteraction = class {
23378
23488
  applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
23379
23489
  }
23380
23490
  d.constrainHandleDrag(this.state.handle);
23381
- } else {
23382
- const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
23383
- this.snapAt = null;
23384
- const dt = pt.time - this.state.grab.time;
23385
- const dp = pt.price - this.state.grab.price;
23386
- d.anchors = d.translateBody(dt, dp, this.state.orig);
23387
23491
  }
23388
23492
  this.deps.changed();
23389
23493
  }
@@ -23403,16 +23507,46 @@ var DrawingInteraction = class {
23403
23507
  }
23404
23508
  return;
23405
23509
  }
23510
+ if (this.state.kind === "marquee") {
23511
+ const rect = this.marqueeRect();
23512
+ this.state = { kind: "idle" };
23513
+ if (rect.w > DRAG_SLOP2 || rect.h > DRAG_SLOP2) this.selectWithin(rect);
23514
+ this.deps.changed();
23515
+ return;
23516
+ }
23406
23517
  if (this.state.kind !== "pressed") return;
23407
- const { id, moved } = this.state;
23518
+ const { id, moved, riders, mod, clones } = this.state;
23408
23519
  this.state = { kind: "idle" };
23409
23520
  const d = this.byId(id);
23410
- if (moved) {
23411
- if (d) this.deps.emit({ kind: "edit", doc: d.serialize() });
23412
- this.deps.changed();
23413
- } else {
23414
- this.deps.openSettings(id, x, y);
23521
+ if (!moved) {
23522
+ if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
23523
+ else this.deps.openSettings(id, x, y);
23524
+ return;
23525
+ }
23526
+ if (clones) {
23527
+ this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
23528
+ } else if (d) {
23529
+ const docs = [d.serialize()];
23530
+ for (const r of riders) {
23531
+ const rd = this.byId(r.id);
23532
+ if (rd) docs.push(rd.serialize());
23533
+ }
23534
+ if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
23535
+ else this.deps.emit({ kind: "edit", doc: docs[0] });
23536
+ }
23537
+ this.deps.changed();
23538
+ }
23539
+ /** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
23540
+ * so successive sweeps accumulate). No change when the box touches nothing new. */
23541
+ selectWithin(rect) {
23542
+ const proj = this.deps.projector();
23543
+ const ids = [...this.deps.selectedIds()];
23544
+ for (const d of this.deps.drawings()) {
23545
+ if (!d.visible || ids.includes(d.id)) continue;
23546
+ const b = d.bounds(proj);
23547
+ if (b && rectsIntersect(rect, b)) ids.push(d.id);
23415
23548
  }
23549
+ if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
23416
23550
  }
23417
23551
  /** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
23418
23552
  cancel() {
@@ -23425,8 +23559,15 @@ var DrawingInteraction = class {
23425
23559
  return true;
23426
23560
  }
23427
23561
  if (this.state.kind === "pressed") {
23428
- const d = this.byId(this.state.id);
23429
- if (d && this.state.moved) d.anchors = this.state.orig.map((o) => ({ time: o.time, price: o.price }));
23562
+ if (this.state.moved && !this.state.clones) {
23563
+ const d = this.byId(this.state.id);
23564
+ if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
23565
+ }
23566
+ this.state = { kind: "idle" };
23567
+ this.deps.changed();
23568
+ return true;
23569
+ }
23570
+ if (this.state.kind === "marquee") {
23430
23571
  this.state = { kind: "idle" };
23431
23572
  this.deps.changed();
23432
23573
  return true;
@@ -23541,6 +23682,9 @@ var DrawingInteraction = class {
23541
23682
  function cloneAnchors(d) {
23542
23683
  return d.anchors.map((a) => ({ time: a.time, price: a.price }));
23543
23684
  }
23685
+ function rectsIntersect(a, b) {
23686
+ 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;
23687
+ }
23544
23688
  function applyFree(anchor, pt, free) {
23545
23689
  if (free === "both" || free === "x") anchor.time = pt.time;
23546
23690
  if (free === "both" || free === "y") anchor.price = pt.price;
@@ -23926,10 +24070,27 @@ function btn(label, primary, onClick) {
23926
24070
  }
23927
24071
 
23928
24072
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
24073
+ var MIXED = /* @__PURE__ */ Symbol("mixed");
24074
+ function sharedPaths(drawings) {
24075
+ const [first, ...rest] = drawings;
24076
+ const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
24077
+ for (const d of rest) {
24078
+ const own = new Set(d.schema().fields.map((f) => f.path));
24079
+ for (const p of paths) if (!own.has(p)) paths.delete(p);
24080
+ }
24081
+ return paths;
24082
+ }
24083
+ function commonValue(drawings, read) {
24084
+ const first = read(drawings[0]);
24085
+ return drawings.every((d) => read(d) === first) ? first : MIXED;
24086
+ }
24087
+ function distinctValues(drawings, read) {
24088
+ return [...new Set(drawings.map(read))];
24089
+ }
23929
24090
  var BTN = 30;
23930
24091
  var ICON = 21;
23931
24092
  var STYLE_ID3 = "vela-drawing-popup-styles";
23932
- var STYLE_REV = "3";
24093
+ var STYLE_REV = "4";
23933
24094
  function ensureStyles3() {
23934
24095
  if (typeof document === "undefined") return;
23935
24096
  const existing = document.getElementById(STYLE_ID3);
@@ -23941,6 +24102,8 @@ function ensureStyles3() {
23941
24102
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
23942
24103
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
23943
24104
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
24105
+ /* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
24106
+ .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;}
23944
24107
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
23945
24108
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
23946
24109
  .vela-dpop-item[data-active='1']{background:var(--vela-active);}
@@ -23972,7 +24135,7 @@ var DrawingSettingsPopup = class {
23972
24135
  if (this.isOwnChrome(node)) return;
23973
24136
  const cb = this.onClose;
23974
24137
  this.close();
23975
- cb?.();
24138
+ cb?.(e);
23976
24139
  };
23977
24140
  this.theme = theme;
23978
24141
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
@@ -23997,16 +24160,27 @@ var DrawingSettingsPopup = class {
23997
24160
  contains(node) {
23998
24161
  return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
23999
24162
  }
24000
- /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
24001
- * `onClose` fires on an outside-click dismissal (not a programmatic close). */
24002
- open(drawing, anchor, actions, onClose) {
24163
+ /** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
24164
+ * their `anchor` box. With several drawings the bar shows only the controls they ALL support,
24165
+ * a control whose values differ reads as mixed, and every edit applies to all of them —
24166
+ * per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
24167
+ * `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
24168
+ * programmatic close), with the dismissing press when there is one, so the caller can tell a
24169
+ * modifier press (multi-select) from a plain one. */
24170
+ open(drawings, anchor, actions, onClose) {
24003
24171
  this.close();
24172
+ const drawing = drawings[0];
24173
+ if (!drawing) return;
24004
24174
  ensureStyles3();
24005
24175
  this.onClose = onClose ?? null;
24006
24176
  const t = this.theme;
24177
+ const multi = drawings.length > 1;
24007
24178
  const schema = drawing.schema();
24008
- const paths = new Set(schema.fields.map((f) => f.path));
24009
- const textOnBar = schema.textIsContent === true || !paths.has("text.value");
24179
+ const paths = sharedPaths(drawings);
24180
+ const common = (read) => commonValue(drawings, read);
24181
+ const every = (pred) => drawings.every(pred);
24182
+ const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
24183
+ const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
24010
24184
  const el = document.createElement("div");
24011
24185
  el.className = "vela-dpop";
24012
24186
  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;`;
@@ -24020,75 +24194,84 @@ var DrawingSettingsPopup = class {
24020
24194
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
24021
24195
  bar.appendChild(this.dragHandle());
24022
24196
  if (paths.has("glyph")) {
24023
- const cur = drawing.glyph ?? GLYPH_OPTIONS[0];
24197
+ const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
24024
24198
  bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
24025
24199
  }
24026
24200
  if (paths.has("size")) {
24027
- const sz = drawing.size ?? "normal";
24201
+ const sz = common((d) => d.size ?? "normal");
24028
24202
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
24029
24203
  }
24030
- if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
24204
+ if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
24205
+ const mag = (d) => d;
24031
24206
  const options = this.lowerTimeframeOptions();
24032
24207
  if (options.length > 0) {
24033
24208
  bar.appendChild(
24034
- this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
24209
+ this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
24035
24210
  label: (v) => magnifierTimeframeLabel(String(v)),
24036
24211
  labelInTrigger: true
24037
24212
  })
24038
24213
  );
24039
24214
  }
24040
- bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
24041
- bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
24215
+ bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
24216
+ bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
24042
24217
  }
24043
- 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 })));
24218
+ 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"));
24044
24219
  if (paths.has("style.lineWidth")) {
24045
- const wf = schema.fields.find((f) => f.path === "style.lineWidth");
24046
- if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
24047
- bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
24220
+ const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
24221
+ const wf = widthField2(drawing);
24222
+ const width = common((d) => d.style.lineWidth);
24223
+ const markers = every((d) => {
24224
+ const f = widthField2(d);
24225
+ return f?.kind === "number" && (f.min ?? 1) > 1;
24226
+ });
24227
+ if (markers && wf?.kind === "number") {
24228
+ bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
24048
24229
  } else {
24049
- 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 }));
24230
+ 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 }));
24050
24231
  }
24051
24232
  }
24052
- 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 }));
24053
- 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 })));
24233
+ 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 }));
24234
+ if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
24054
24235
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
24055
- const isPosition = paths.has("riskPercent") && drawing instanceof PositionTool;
24236
+ const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
24056
24237
  if (isPosition) {
24057
- const pos = drawing;
24058
- bar.appendChild(this.colorButton("Profit zone", BUCKET_ICON, pos.profitColor, (v) => actions.patch({ profitColor: v })));
24059
- bar.appendChild(this.colorButton("Loss zone", BUCKET_ICON, pos.lossColor, (v) => actions.patch({ lossColor: v })));
24238
+ const pos = (d) => d;
24239
+ bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
24240
+ bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
24060
24241
  }
24061
- const reg = drawing.reg;
24062
- if (paths.has("reg.midColor") && reg) {
24242
+ const regOf = (d) => d.reg;
24243
+ if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
24244
+ const reg = (d) => regOf(d);
24063
24245
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
24064
- bar.appendChild(this.colorButton("Midline color", BRUSH_ICON, reg.midColor, (v) => actions.patch({ "reg.midColor": v })));
24065
- bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
24066
- bar.appendChild(this.colorButton("Upper line color", BRUSH_ICON, reg.upperColor, (v) => actions.patch({ "reg.upperColor": v })));
24067
- bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
24068
- bar.appendChild(this.colorButton("Lower line color", BRUSH_ICON, reg.lowerColor, (v) => actions.patch({ "reg.lowerColor": v })));
24069
- bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
24070
- bar.appendChild(this.colorButton("Upper fill", BUCKET_ICON, reg.upperFill, (v) => actions.patch({ "reg.upperFill": v })));
24071
- bar.appendChild(this.colorButton("Lower fill", BUCKET_ICON, reg.lowerFill, (v) => actions.patch({ "reg.lowerFill": v })));
24072
- bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
24073
- }
24074
- const vwap2 = drawing.vwap;
24075
- if (paths.has("vwap.midColor") && vwap2) {
24246
+ bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
24247
+ bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
24248
+ bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
24249
+ 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 }));
24250
+ bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
24251
+ 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 }));
24252
+ bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
24253
+ bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
24254
+ bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
24255
+ }
24256
+ const vwapOf = (d) => d.vwap;
24257
+ if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
24258
+ const vwap2 = (d) => vwapOf(d);
24076
24259
  const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
24077
24260
  const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
24078
- bar.appendChild(this.colorButton("VWAP color", BRUSH_ICON, vwap2.midColor, (v) => actions.patch({ "vwap.midColor": v })));
24079
- bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
24261
+ bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
24262
+ bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
24080
24263
  bar.appendChild(
24081
- this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
24264
+ this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
24082
24265
  label: (v) => `${v}\u03C3`,
24083
24266
  labelInTrigger: true
24084
24267
  })
24085
24268
  );
24086
- bar.appendChild(this.colorButton("Upper band color", BRUSH_ICON, vwap2.upperColor, (v) => actions.patch({ "vwap.upperColor": v })));
24087
- bar.appendChild(this.colorButton("Lower band color", BRUSH_ICON, vwap2.lowerColor, (v) => actions.patch({ "vwap.lowerColor": v })));
24088
- bar.appendChild(this.colorButton("Band fill", BUCKET_ICON, vwap2.bandFill, (v) => actions.patch({ "vwap.bandFill": v })));
24269
+ bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
24270
+ bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
24271
+ bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
24089
24272
  }
24090
24273
  if (paths.has("maxCurvature")) {
24091
- const cur = drawing.maxCurvature ?? 24;
24274
+ const cur = common((d) => d.maxCurvature ?? 24);
24092
24275
  bar.appendChild(
24093
24276
  this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
24094
24277
  label: (v) => `n=${v}`,
@@ -24097,7 +24280,7 @@ var DrawingSettingsPopup = class {
24097
24280
  );
24098
24281
  }
24099
24282
  if (paths.has("mach")) {
24100
- const cur = drawing.mach ?? 2;
24283
+ const cur = common((d) => d.mach ?? 2);
24101
24284
  bar.appendChild(
24102
24285
  this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
24103
24286
  label: (v) => `M=${v}`,
@@ -24106,7 +24289,7 @@ var DrawingSettingsPopup = class {
24106
24289
  );
24107
24290
  }
24108
24291
  if (paths.has("waveCount")) {
24109
- const cur = drawing.waveCount ?? 6;
24292
+ const cur = common((d) => d.waveCount ?? 6);
24110
24293
  bar.appendChild(
24111
24294
  this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
24112
24295
  label: (v) => `${v}`,
@@ -24114,26 +24297,27 @@ var DrawingSettingsPopup = class {
24114
24297
  })
24115
24298
  );
24116
24299
  }
24117
- const toggles = drawing;
24118
- if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, toggles.showPrice !== false, (v) => actions.patch({ showPrice: v })));
24119
- if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, toggles.showDate !== false, (v) => actions.patch({ showDate: v })));
24120
- 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 })));
24121
- 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 }));
24122
- 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 })));
24123
- 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 })));
24124
- if (paths.has("text.value")) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
24300
+ const flags = (d) => d;
24301
+ if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
24302
+ if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
24303
+ if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
24304
+ 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 }));
24305
+ const textField = !multi && paths.has("text.value");
24306
+ if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
24307
+ if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
24308
+ if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
24125
24309
  const editableLevels = drawing.editableLevels();
24126
- if (editableLevels && !(drawing instanceof MachFigure)) {
24127
- const fib = drawing;
24310
+ if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
24311
+ const fib = (d) => d;
24128
24312
  const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
24129
- bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
24130
- bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
24313
+ bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
24314
+ bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
24131
24315
  }
24132
24316
  bar.appendChild(this.divider());
24133
- if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
24134
- if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
24135
- if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
24136
- bar.appendChild(this.toggle("Lock", LOCK_ICON, drawing.locked, (v) => actions.setLocked(v)));
24317
+ if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
24318
+ if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
24319
+ if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
24320
+ bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
24137
24321
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
24138
24322
  del.style.color = "var(--vela-danger)";
24139
24323
  bar.appendChild(del);
@@ -24319,7 +24503,7 @@ var DrawingSettingsPopup = class {
24319
24503
  });
24320
24504
  return b;
24321
24505
  }
24322
- /** The trailing kebab overflow — z-order actions + reset settings. */
24506
+ /** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
24323
24507
  kebabButton(actions) {
24324
24508
  const b = this.base("More");
24325
24509
  b.innerHTML = sized(KEBAB_ICON);
@@ -24331,6 +24515,7 @@ var DrawingSettingsPopup = class {
24331
24515
  return;
24332
24516
  }
24333
24517
  this.openActionMenu(b, [
24518
+ { icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
24334
24519
  { icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
24335
24520
  { icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
24336
24521
  { icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
@@ -24487,26 +24672,30 @@ var DrawingSettingsPopup = class {
24487
24672
  this.tipEl?.remove();
24488
24673
  this.tipEl = null;
24489
24674
  }
24490
- toggle(tip, icon2, active, onChange) {
24675
+ /** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
24676
+ * the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
24677
+ * toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
24678
+ toggle(tip, icon2, active, onChange, off) {
24491
24679
  const b = this.base(tip);
24492
- b.innerHTML = sized(icon2);
24493
24680
  const set = (on2) => {
24494
- b.dataset.active = on2 ? "1" : "0";
24681
+ b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
24682
+ b.innerHTML = sized(off && on2 === false ? off : icon2);
24495
24683
  };
24496
24684
  set(active);
24497
24685
  let on = active;
24498
24686
  b.addEventListener("click", () => {
24499
- on = !on;
24687
+ on = on === MIXED ? true : !on;
24500
24688
  set(on);
24501
24689
  onChange(on);
24502
24690
  });
24503
24691
  return b;
24504
24692
  }
24505
24693
  /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
24506
- * ladder — the value is clamped to the schema's declared min/max on commit. */
24694
+ * ladder — the value is clamped to the schema's declared min/max on commit. A mixed
24695
+ * value shows an empty field with a dash until a number is typed for all. */
24507
24696
  widthInput(tip, value, min, max, step, onChange) {
24508
24697
  const ni = new NumberInput({
24509
- value,
24698
+ value: value === MIXED ? min : value,
24510
24699
  min,
24511
24700
  max,
24512
24701
  step,
@@ -24518,13 +24707,18 @@ var DrawingSettingsPopup = class {
24518
24707
  title: tip,
24519
24708
  onChange
24520
24709
  });
24710
+ if (value === MIXED) {
24711
+ ni.input.value = "";
24712
+ ni.input.placeholder = "\u2014";
24713
+ }
24521
24714
  ni.el.dataset.tip = tip;
24522
24715
  trapChartKeys(ni.el);
24523
24716
  return ni.el;
24524
24717
  }
24525
24718
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
24526
24719
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
24527
- * opens a floating list of every option so a value is one click away (no cycling through). */
24720
+ * opens a floating list of every option so a value is one click away (no cycling through).
24721
+ * A mixed value shows a dash in the trigger and highlights no row. */
24528
24722
  dropdown(tip, values, current, render, onChange, opts = {}) {
24529
24723
  const b = this.base(tip);
24530
24724
  b.style.width = "auto";
@@ -24534,14 +24728,20 @@ var DrawingSettingsPopup = class {
24534
24728
  let cur = current;
24535
24729
  const paint = (v) => {
24536
24730
  b.replaceChildren();
24537
- const glyph = render(v);
24731
+ if (v === MIXED) {
24732
+ const dash = document.createElement("span");
24733
+ dash.textContent = "\u2014";
24734
+ dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
24735
+ b.appendChild(dash);
24736
+ }
24737
+ const glyph = v === MIXED ? "" : render(v);
24538
24738
  if (glyph) {
24539
24739
  const ic = document.createElement("span");
24540
24740
  ic.style.cssText = "display:flex;";
24541
24741
  ic.innerHTML = sized(glyph);
24542
24742
  b.appendChild(ic);
24543
24743
  }
24544
- if (opts.label && opts.labelInTrigger) {
24744
+ if (opts.label && opts.labelInTrigger && v !== MIXED) {
24545
24745
  const tx = document.createElement("span");
24546
24746
  tx.textContent = opts.label(v);
24547
24747
  tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
@@ -24611,8 +24811,10 @@ var DrawingSettingsPopup = class {
24611
24811
  }
24612
24812
  /** A color control: an `icon` over a thin **colored underline** showing the current
24613
24813
  * value (the common idiom), with an invisible native picker overlaid to edit it.
24614
- * `iconSize` shrinks it for the floating text controls. */
24615
- colorButton(tip, icon2, color, onChange, iconSize = 17) {
24814
+ * `iconSize` shrinks it for the floating text controls. A mixed color paints the
24815
+ * underline striped with the colors `used` across the selection, and the picker then
24816
+ * leads with those colors so unifying onto one of them is a single click. */
24817
+ colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
24616
24818
  const b = document.createElement("button");
24617
24819
  b.type = "button";
24618
24820
  b.dataset.tip = tip;
@@ -24622,13 +24824,14 @@ var DrawingSettingsPopup = class {
24622
24824
  ic.style.cssText = "display:flex;";
24623
24825
  ic.innerHTML = sized(icon2, iconSize);
24624
24826
  const bar = document.createElement("span");
24625
- bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
24827
+ bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
24626
24828
  b.append(ic, bar);
24627
24829
  let cur = color;
24628
24830
  b.addEventListener("pointerdown", (e) => e.stopPropagation());
24629
24831
  b.addEventListener("click", (e) => {
24630
24832
  e.stopPropagation();
24631
- this.toggleColorPopover(b, cur, (v) => {
24833
+ const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
24834
+ this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
24632
24835
  cur = v;
24633
24836
  bar.style.background = v;
24634
24837
  onChange(v);
@@ -24637,20 +24840,42 @@ var DrawingSettingsPopup = class {
24637
24840
  return b;
24638
24841
  }
24639
24842
  /** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
24640
- toggleColorPopover(anchor, color, onChange) {
24843
+ toggleColorPopover(anchor, color, used, onChange) {
24641
24844
  if (this.colorOwner === anchor) {
24642
24845
  this.closeColorPopover();
24643
24846
  return;
24644
24847
  }
24645
- this.openColorPopover(anchor, color, onChange);
24848
+ this.openColorPopover(anchor, color, used, onChange);
24646
24849
  }
24647
- /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
24648
- openColorPopover(anchor, color, onChange) {
24850
+ /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
24851
+ * `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
24852
+ openColorPopover(anchor, color, used, onChange) {
24649
24853
  const t = this.theme;
24650
24854
  this.colorPop = this.hostFloat(anchor, {
24651
24855
  zIndex: 25,
24652
24856
  padding: "10px",
24653
- fill: (el) => {
24857
+ fill: (el, pop) => {
24858
+ if (used.length > 1) {
24859
+ const row = document.createElement("div");
24860
+ row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
24861
+ const lbl = document.createElement("span");
24862
+ lbl.textContent = "In use";
24863
+ lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
24864
+ row.appendChild(lbl);
24865
+ for (const c of used) {
24866
+ const sw = document.createElement("button");
24867
+ sw.type = "button";
24868
+ sw.dataset.tip = c;
24869
+ sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
24870
+ sw.addEventListener("click", (e) => {
24871
+ e.stopPropagation();
24872
+ pop.hide();
24873
+ onChange(c);
24874
+ });
24875
+ row.appendChild(sw);
24876
+ }
24877
+ el.appendChild(row);
24878
+ }
24654
24879
  el.appendChild(buildColorPicker(color, t, onChange));
24655
24880
  }
24656
24881
  });
@@ -24671,6 +24896,7 @@ var TYPE_ICON = icon("type");
24671
24896
  var PRICE_DELTA_ICON = icon("price-delta");
24672
24897
  var DATE_DELTA_ICON = icon("date-delta");
24673
24898
  var LOCK_ICON = icon("lock");
24899
+ var UNLOCK_ICON = icon("unlock");
24674
24900
  var FRONT_ICON = icon("bring-front");
24675
24901
  var BACK_ICON = icon("send-back");
24676
24902
  var TRASH_ICON = icon("trash");
@@ -24678,6 +24904,7 @@ var BOLD_ICON = icon("bold");
24678
24904
  var ITALIC_ICON = icon("italic");
24679
24905
  var GRIP_ICON = icon("grip");
24680
24906
  var KEBAB_ICON = icon("kebab");
24907
+ var CLONE_ICON = icon("clone");
24681
24908
  var RESET_ICON = icon("reset");
24682
24909
  var GEAR_ICON = icon("gear");
24683
24910
  var CHEVRON_ICON = icon("chevron-down");
@@ -24686,6 +24913,12 @@ var BANDS_ICON = icon("bands");
24686
24913
  var DEDEKIND_ICON2 = icon("dedekind");
24687
24914
  var SONIC_ICON2 = icon("sonic");
24688
24915
  var SUPERSONIC_ICON2 = icon("supersonic");
24916
+ function stripes(colors) {
24917
+ const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
24918
+ const step = 100 / bands.length;
24919
+ const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
24920
+ return `linear-gradient(90deg,${stops})`;
24921
+ }
24689
24922
  function lineIcon(width, style) {
24690
24923
  const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
24691
24924
  return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
@@ -25601,6 +25834,20 @@ var UserDrawingController = class {
25601
25834
  // reported exactly when it actually changes).
25602
25835
  this.textEditor = null;
25603
25836
  this.painter = new DrawingPainter();
25837
+ /** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
25838
+ * shrinking selection does with it. */
25839
+ this.popupMulti = false;
25840
+ /** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
25841
+ * selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
25842
+ * selection or sweep a marquee onto it), or it landed on a drawing that is already selected
25843
+ * (the canvas handler ran first and is holding it for a drag or a click), so the drawings
25844
+ * whose bar just closed must stay selected. */
25845
+ this.onPopupDismissed = (e) => {
25846
+ if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
25847
+ const pressed = this.interaction.pressedId();
25848
+ if (pressed && this.selectedIds.has(pressed)) return;
25849
+ this.clearSelection();
25850
+ };
25604
25851
  this.ctx = canvas.getContext("2d");
25605
25852
  this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
25606
25853
  this.toolbar = new DrawingToolbar(
@@ -25777,6 +26024,12 @@ var UserDrawingController = class {
25777
26024
  }
25778
26025
  setSelection(ids) {
25779
26026
  this.selectedIds = new Set(ids);
26027
+ if (ids.length >= 2) this.openSettingsForSelection(ids);
26028
+ else if (this.popupMulti) {
26029
+ this.popupMulti = false;
26030
+ this.popup.close();
26031
+ if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
26032
+ }
25780
26033
  this.render();
25781
26034
  }
25782
26035
  /** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
@@ -25822,12 +26075,16 @@ var UserDrawingController = class {
25822
26075
  });
25823
26076
  }
25824
26077
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
25825
- * Shared by the eraser (click + drag) and the middle-click shortcut. */
25826
- deleteAt(x, y) {
26078
+ * Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
26079
+ * `withSelection` so a hit on a member of a multi-selection removes the selection's
26080
+ * unlocked members — whichever member was hit, locked or not. */
26081
+ deleteAt(x, y, withSelection = false) {
25827
26082
  const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
25828
- if (!hit || hit.locked) return false;
26083
+ if (!hit) return false;
26084
+ const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
26085
+ if (ids.length === 0) return false;
25829
26086
  this.popup.close();
25830
- this.emit({ kind: "delete", ids: [hit.id] });
26087
+ this.emit({ kind: "delete", ids });
25831
26088
  return true;
25832
26089
  }
25833
26090
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
@@ -25853,7 +26110,14 @@ var UserDrawingController = class {
25853
26110
  this.render();
25854
26111
  }
25855
26112
  }
25856
- pointerDown(x, y, snap = "off", shift2 = false) {
26113
+ /** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
26114
+ * the selection). Returns false when a mode/tool is active (the normal press path owns it). */
26115
+ beginMarqueeAt(x, y) {
26116
+ if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
26117
+ this.render();
26118
+ return true;
26119
+ }
26120
+ pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
25857
26121
  if (this.eraserMode) {
25858
26122
  this.erasing = true;
25859
26123
  this.deleteAt(x, y);
@@ -25873,9 +26137,9 @@ var UserDrawingController = class {
25873
26137
  return;
25874
26138
  }
25875
26139
  }
25876
- this.interaction.down(x, y, snap, shift2);
26140
+ this.interaction.down(x, y, snap, shift2, mod);
25877
26141
  }
25878
- pointerMove(x, y, snap = "off", shift2 = false) {
26142
+ pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
25879
26143
  if (this.eraserMode) {
25880
26144
  if (this.erasing) this.deleteAt(x, y);
25881
26145
  return;
@@ -25887,12 +26151,14 @@ var UserDrawingController = class {
25887
26151
  return;
25888
26152
  }
25889
26153
  this.interaction.move(x, y, snap, shift2);
25890
- this.updateHover(x, y);
26154
+ this.updateHover(x, y, mod);
25891
26155
  }
25892
- /** Track which drawing is hovered so its handles appear on hover (and only then). */
25893
- updateHover(x, y) {
26156
+ /** Track which drawing is hovered so its handles appear on hover (and only then). While
26157
+ * Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
26158
+ * selected, so a hovered candidate doesn't already read as selected. */
26159
+ updateHover(x, y, mod = false) {
25894
26160
  let id = null;
25895
- if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
26161
+ if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
25896
26162
  id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
25897
26163
  }
25898
26164
  if (id !== this.hoveredId) {
@@ -25912,7 +26178,12 @@ var UserDrawingController = class {
25912
26178
  this.render();
25913
26179
  return;
25914
26180
  }
26181
+ const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
25915
26182
  this.interaction.up(x, y);
26183
+ if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
26184
+ if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
26185
+ else this.openSettingsById(dragged, x, y);
26186
+ }
25916
26187
  }
25917
26188
  /** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
25918
26189
  toggleMeasure() {
@@ -26177,7 +26448,7 @@ var UserDrawingController = class {
26177
26448
  this.emit({ kind: "duplicate", ids: this.selectionIds() });
26178
26449
  break;
26179
26450
  case "delete": {
26180
- const ids = this.selectedIds.size ? this.selectionIds() : this.hoveredId ? [this.hoveredId] : [];
26451
+ const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
26181
26452
  if (ids.length) {
26182
26453
  this.popup.close();
26183
26454
  this.emit({ kind: "delete", ids });
@@ -26193,6 +26464,9 @@ var UserDrawingController = class {
26193
26464
  selectionIds() {
26194
26465
  return [...this.selectedIds];
26195
26466
  }
26467
+ deletableSelection() {
26468
+ return deletableSelection(this.selectedIds, this.drawings);
26469
+ }
26196
26470
  /** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
26197
26471
  nudgeSelection(dx, dy) {
26198
26472
  const proj = this.deps.projector();
@@ -26301,6 +26575,8 @@ var UserDrawingController = class {
26301
26575
  this.painter.seriesLook = this.deps.seriesLook();
26302
26576
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
26303
26577
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
26578
+ const clones = this.interaction.dragClones();
26579
+ if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
26304
26580
  this.layoutTextEditor();
26305
26581
  const ghost = this.interaction.ghost();
26306
26582
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
@@ -26316,6 +26592,8 @@ var UserDrawingController = class {
26316
26592
  const my = m ? proj.yOf(m.point.price, m.paneId) : null;
26317
26593
  if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
26318
26594
  if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
26595
+ const marquee = this.interaction.marqueeRect();
26596
+ if (marquee) this.painter.paintMarquee(ctx, marquee);
26319
26597
  }
26320
26598
  destroy() {
26321
26599
  this.closeTextEditor();
@@ -26333,15 +26611,80 @@ var UserDrawingController = class {
26333
26611
  if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
26334
26612
  this.render();
26335
26613
  }
26614
+ /** Report the live drawings' current state as one edit (one undo step, however many). */
26615
+ emitEdits(drawings) {
26616
+ const docs = drawings.map((d) => d.serialize());
26617
+ if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
26618
+ else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
26619
+ }
26620
+ /** The one bar for a multi-selection: it shows the controls all of its drawings share and
26621
+ * every action applies to all of them. Floats clear of their combined bounds. */
26622
+ openSettingsForSelection(ids) {
26623
+ const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
26624
+ const drawings = live();
26625
+ if (drawings.length < 2) return;
26626
+ const proj = this.deps.projector();
26627
+ let anchor = null;
26628
+ for (const d of drawings) {
26629
+ const b = d.bounds(proj);
26630
+ if (!b) continue;
26631
+ if (!anchor) anchor = { ...b };
26632
+ else {
26633
+ const right = Math.max(anchor.x + anchor.w, b.x + b.w);
26634
+ const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
26635
+ anchor.x = Math.min(anchor.x, b.x);
26636
+ anchor.y = Math.min(anchor.y, b.y);
26637
+ anchor.w = right - anchor.x;
26638
+ anchor.h = bottom - anchor.y;
26639
+ }
26640
+ }
26641
+ this.popupMulti = true;
26642
+ this.emit({ kind: "settings", id: drawings[0].id });
26643
+ this.popup.open(drawings, anchor, {
26644
+ resolve: () => live()[0] ?? null,
26645
+ patch: (p) => {
26646
+ const ds = live();
26647
+ for (const d of ds) d.applySettings(p);
26648
+ this.render();
26649
+ this.emitEdits(ds);
26650
+ },
26651
+ setLocked: (v) => {
26652
+ const ds = live();
26653
+ for (const d of ds) d.locked = v;
26654
+ this.emitEdits(ds);
26655
+ },
26656
+ reorder: (to) => {
26657
+ for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
26658
+ },
26659
+ duplicate: () => {
26660
+ this.popup.close();
26661
+ this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
26662
+ },
26663
+ resetSettings: () => {
26664
+ const ds = live();
26665
+ for (const d of ds) resetDrawingSettings(d);
26666
+ this.render();
26667
+ this.emitEdits(ds);
26668
+ this.openSettingsForSelection(ids);
26669
+ },
26670
+ remove: () => {
26671
+ const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
26672
+ if (ids2.length === 0) return;
26673
+ this.popup.close();
26674
+ this.emit({ kind: "delete", ids: ids2 });
26675
+ }
26676
+ }, this.onPopupDismissed);
26677
+ }
26336
26678
  /** A click on a drawing opens its settings toolbar (text labels edit text here too). */
26337
26679
  openSettingsById(id, _x, _y) {
26338
26680
  const drawing = this.drawings.find((d) => d.id === id);
26339
26681
  if (!drawing) return;
26340
26682
  const live = () => this.drawings.find((d) => d.id === id);
26683
+ this.popupMulti = false;
26341
26684
  this.emit({ kind: "select", ids: [id] });
26342
26685
  this.emit({ kind: "settings", id });
26343
26686
  const anchor = drawing.bounds(this.deps.projector());
26344
- this.popup.open(drawing, anchor, {
26687
+ this.popup.open([drawing], anchor, {
26345
26688
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
26346
26689
  // position tool's price fields, where one edit can flip another level) resolves fresh.
26347
26690
  resolve: () => live() ?? null,
@@ -26359,6 +26702,11 @@ var UserDrawingController = class {
26359
26702
  this.emit({ kind: "edit", doc: d.serialize() });
26360
26703
  },
26361
26704
  reorder: (to) => this.emit({ kind: "reorder", id, to }),
26705
+ duplicate: () => {
26706
+ this.finishTextEditor(true);
26707
+ this.popup.close();
26708
+ this.emit({ kind: "duplicate", ids: [id] });
26709
+ },
26362
26710
  resetSettings: () => {
26363
26711
  const d = live();
26364
26712
  if (!d) return;
@@ -26381,7 +26729,13 @@ var UserDrawingController = class {
26381
26729
  this.popup.close();
26382
26730
  this.emit({ kind: "delete", ids: [id] });
26383
26731
  }
26384
- }, () => this.clearSelection());
26732
+ }, this.onPopupDismissed);
26733
+ }
26734
+ /** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
26735
+ * does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
26736
+ * selection whose bar was dismissed by a modifier press) — this covers those. */
26737
+ deselect() {
26738
+ this.clearSelection();
26385
26739
  }
26386
26740
  /** Report placement progress upstream (`draft` intent) so the drawings sync can
26387
26741
  * mirror the ghost live on linked charts. Called from every render; only actual
@@ -26407,6 +26761,13 @@ var UserDrawingController = class {
26407
26761
  }, 0);
26408
26762
  return;
26409
26763
  }
26764
+ if (i.kind === "clone") {
26765
+ const before = new Set(this.drawings.map((d) => d.id));
26766
+ this.intentCb?.(i);
26767
+ const fresh = this.drawings.filter((d) => !before.has(d.id));
26768
+ if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
26769
+ return;
26770
+ }
26410
26771
  this.intentCb?.(i);
26411
26772
  }
26412
26773
  };
@@ -28718,7 +29079,10 @@ var NativeRenderer = class {
28718
29079
  zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
28719
29080
  fling: (v) => this.fling(v),
28720
29081
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
28721
- onClick: (x) => this.handleClick(x),
29082
+ onClick: (x) => {
29083
+ this.userDrawings?.deselect();
29084
+ this.handleClick(x);
29085
+ },
28722
29086
  onAxisLongPress: (axis, x, y) => {
28723
29087
  for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
28724
29088
  },
@@ -28736,11 +29100,13 @@ var NativeRenderer = class {
28736
29100
  // User drawings claim a gesture before pan when armed / over a drawing.
28737
29101
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
28738
29102
  drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
28739
- drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
29103
+ drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
29104
+ drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
29105
+ // middle-click: a selected hit takes the whole selection
28740
29106
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
28741
29107
  drawingsSnapMode: () => this.snapMode,
28742
- drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
28743
- drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
29108
+ drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
29109
+ drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
28744
29110
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
28745
29111
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
28746
29112
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -31103,6 +31469,10 @@ function str3(inputs, key, fallback) {
31103
31469
  const v = inputs[key];
31104
31470
  return typeof v === "string" && v !== "" ? v : fallback;
31105
31471
  }
31472
+ function bool(inputs, key, fallback) {
31473
+ const v = inputs[key];
31474
+ return typeof v === "boolean" ? v : fallback;
31475
+ }
31106
31476
  function toPoints(bars, values, colors) {
31107
31477
  const pts = new Array(bars.length);
31108
31478
  for (let i = 0; i < bars.length; i++) {
@@ -32634,10 +33004,6 @@ var aroon = {
32634
33004
  plots: [
32635
33005
  { key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
32636
33006
  { key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
32637
- ],
32638
- levels: [
32639
- { key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
32640
- { key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
32641
33007
  ]
32642
33008
  };
32643
33009
  }
@@ -33135,8 +33501,10 @@ var volumeSpecs = [
33135
33501
  var DAY_MS2 = 864e5;
33136
33502
  function periodKey(anchor, time) {
33137
33503
  if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
33138
- if (anchor === "Month") {
33504
+ if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
33139
33505
  const d = new Date(time);
33506
+ if (anchor === "Year") return d.getUTCFullYear();
33507
+ if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
33140
33508
  return d.getUTCFullYear() * 12 + d.getUTCMonth();
33141
33509
  }
33142
33510
  return Math.floor(time / DAY_MS2);
@@ -33188,40 +33556,86 @@ var parabolicSar = {
33188
33556
  return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
33189
33557
  }
33190
33558
  };
33559
+ var VWAP_BANDS_GROUP = "Bands";
33560
+ var VWAP_STYLE = "Style";
33561
+ var VWAP_BANDS = [
33562
+ { on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
33563
+ { on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
33564
+ { on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
33565
+ ];
33191
33566
  var vwap = {
33192
33567
  type: "vwap",
33193
33568
  title: "Volume Weighted Average Price",
33194
33569
  shortTitle: "VWAP",
33195
33570
  overlay: true,
33196
33571
  inputs: [
33197
- { key: "anchor", title: "Anchor", type: "string", defval: "Day", options: ["Day", "Week", "Month"], tooltip: "Where the accumulation resets (UTC periods)" },
33198
- { key: "source", title: "Source", type: "string", defval: "HLC3", options: ["HLC3", "OHLC4", "Close"] },
33199
- colorInput(INFO)
33572
+ { key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
33573
+ sourceInput("HLC3"),
33574
+ // Each band is one row: its toggle leads, the multiplier follows unlabeled.
33575
+ ...VWAP_BANDS.flatMap((b, i) => [
33576
+ { key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
33577
+ { key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
33578
+ ]),
33579
+ { ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
33580
+ // One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
33581
+ ...VWAP_BANDS.flatMap((b, i) => [
33582
+ { ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
33583
+ { key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
33584
+ { ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
33585
+ ])
33200
33586
  ],
33201
33587
  compute: (bars, inputs) => {
33202
33588
  const anchor = str3(inputs, "anchor", "Day");
33203
- const source = str3(inputs, "source", "HLC3");
33204
- const values = new Array(bars.length).fill(Number.NaN);
33589
+ const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
33590
+ const n = bars.length;
33591
+ const values = new Array(n).fill(Number.NaN);
33592
+ const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
33593
+ mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
33594
+ color: str3(inputs, b.color, b.defColor),
33595
+ fill: bool(inputs, b.fill, true),
33596
+ fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
33597
+ up: new Array(n).fill(Number.NaN),
33598
+ down: new Array(n).fill(Number.NaN)
33599
+ }));
33205
33600
  let period = Number.NaN;
33206
33601
  let cumPV = 0;
33602
+ let cumPV2 = 0;
33207
33603
  let cumV = 0;
33208
- for (let i = 0; i < bars.length; i++) {
33604
+ for (let i = 0; i < n; i++) {
33209
33605
  const b = bars[i];
33210
33606
  const key = periodKey(anchor, b.time);
33211
33607
  if (key !== period) {
33212
33608
  period = key;
33213
33609
  cumPV = 0;
33610
+ cumPV2 = 0;
33214
33611
  cumV = 0;
33215
33612
  }
33216
33613
  const v = b.volume;
33217
33614
  if (v != null && Number.isFinite(v) && v > 0) {
33218
- const tp = source === "Close" ? b.close : source === "OHLC4" ? (b.open + b.high + b.low + b.close) / 4 : (b.high + b.low + b.close) / 3;
33615
+ const tp = src[i];
33219
33616
  cumPV += tp * v;
33617
+ cumPV2 += tp * tp * v;
33220
33618
  cumV += v;
33221
33619
  }
33222
- if (cumV > 0) values[i] = cumPV / cumV;
33223
- }
33224
- return { plots: [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }] };
33620
+ if (cumV <= 0) continue;
33621
+ const mean = cumPV / cumV;
33622
+ const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
33623
+ values[i] = mean;
33624
+ for (const band of bands) {
33625
+ band.up[i] = mean + band.mult * sd;
33626
+ band.down[i] = mean - band.mult * sd;
33627
+ }
33628
+ }
33629
+ const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
33630
+ const fills = [];
33631
+ bands.forEach((band, i) => {
33632
+ plots.push(
33633
+ { key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
33634
+ { key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
33635
+ );
33636
+ if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
33637
+ });
33638
+ return { plots, bands: fills };
33225
33639
  }
33226
33640
  };
33227
33641
  var pivotPoints = {