@seatlayer/core 0.10.1 → 0.11.0

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.
package/dist/index.cjs CHANGED
@@ -956,6 +956,7 @@ var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
956
956
  var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
957
957
  var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
958
958
  var MAX_LABELS = 700;
959
+ var MARQUEE_RING_CAP = 2500;
959
960
  var ISO_ANGLE_DEG = -11.5;
960
961
  var ISO_SQUASH = 0.58;
961
962
  var LIFT_PER_STEP = 58;
@@ -1182,8 +1183,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1182
1183
  this.panLast = null;
1183
1184
  /** Cumulative gesture movement in px — clicks are suppressed after a real pan/pinch. */
1184
1185
  this.moved = 0;
1186
+ /**
1187
+ * Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
1188
+ * points (overlayLayer rides the stage transform); `rect` is the on-canvas
1189
+ * selection band. Null except during an active manage-mode drag.
1190
+ */
1191
+ this.marquee = null;
1192
+ this.marqueeCur = null;
1185
1193
  // ---- keyboard navigation (accessibility) ----------------------------------
1186
1194
  this.onKeyDown = (e) => {
1195
+ if (this.opts.manageMode) {
1196
+ if ((e.metaKey || e.ctrlKey) && (e.key === "a" || e.key === "A")) {
1197
+ e.preventDefault();
1198
+ this.opts.onMarquee?.(this.selectAllSelectable());
1199
+ return;
1200
+ }
1201
+ if (e.key === "Escape" && this.selection.size) {
1202
+ e.preventDefault();
1203
+ this.clearSelection();
1204
+ this.opts.onMarquee?.([]);
1205
+ return;
1206
+ }
1207
+ }
1187
1208
  const dir = e.key === "ArrowLeft" ? { x: -1, y: 0 } : e.key === "ArrowRight" ? { x: 1, y: 0 } : e.key === "ArrowUp" ? { x: 0, y: -1 } : e.key === "ArrowDown" ? { x: 0, y: 1 } : null;
1188
1209
  if (dir) {
1189
1210
  e.preventDefault();
@@ -1206,9 +1227,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1206
1227
  this.pointers.set(e.pointerId, this.toLocal(e));
1207
1228
  if (this.pointers.size === 1) {
1208
1229
  this.moved = 0;
1209
- this.panLast = this.toLocal(e);
1210
1230
  this.pinch = null;
1231
+ if (this.opts.manageMode && this.opts.marqueeSelect && e.pointerType !== "touch" && e.button === 0 && this.getRung() === "seats") {
1232
+ this.beginMarquee(this.toLocal(e));
1233
+ this.panLast = null;
1234
+ } else {
1235
+ this.panLast = this.toLocal(e);
1236
+ }
1211
1237
  } else if (this.pointers.size === 2) {
1238
+ if (this.marquee) this.cancelMarquee();
1212
1239
  const [a, b] = [...this.pointers.values()];
1213
1240
  const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1214
1241
  const s = this.stage.scaleX();
@@ -1227,6 +1254,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1227
1254
  const prev = this.pointers.get(e.pointerId);
1228
1255
  this.moved += Math.hypot(p.x - prev.x, p.y - prev.y);
1229
1256
  this.pointers.set(e.pointerId, p);
1257
+ if (this.marquee) {
1258
+ this.updateMarquee(p);
1259
+ return;
1260
+ }
1230
1261
  if (this.pinch && this.pointers.size >= 2) {
1231
1262
  const [a, b] = [...this.pointers.values()];
1232
1263
  const dist = Math.hypot(b.x - a.x, b.y - a.y);
@@ -1252,6 +1283,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1252
1283
  }
1253
1284
  };
1254
1285
  this.onPointerEnd = (e) => {
1286
+ if (this.marquee) {
1287
+ this.finishMarquee();
1288
+ this.pointers.delete(e.pointerId);
1289
+ if (this.pointers.size === 0) this.panLast = null;
1290
+ return;
1291
+ }
1255
1292
  this.pointers.delete(e.pointerId);
1256
1293
  if (this.pointers.size < 2) this.pinch = null;
1257
1294
  if (this.pointers.size === 1) {
@@ -1487,6 +1524,144 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1487
1524
  }
1488
1525
  if (changed) this.overlayLayer.batchDraw();
1489
1526
  }
1527
+ // ---- manage-mode bulk selection (SDK SeatManager) -------------------------
1528
+ // All option-gated: no-ops (or empty) unless `manageMode` is set, so buyer
1529
+ // surfaces that never pass the flag can't reach any of this.
1530
+ /** Select every selectable seat on the chart (⌘A). Returns the added seats. */
1531
+ selectAllSelectable() {
1532
+ if (!this.opts.manageMode) return [];
1533
+ const ids = [];
1534
+ for (const seat of this.seats) if (this.isSelectable(seat.id)) ids.push(seat.id);
1535
+ return this.selectMany(ids);
1536
+ }
1537
+ /** Select the selectable seats matching these public labels (category/row/
1538
+ * section bulk resolves to labels host-side). Returns the newly added seats. */
1539
+ selectByLabels(labels) {
1540
+ if (!this.opts.manageMode) return [];
1541
+ const want = new Set(labels);
1542
+ const ids = [];
1543
+ for (const seat of this.seats) {
1544
+ if (want.has(seat.label) && this.isSelectable(seat.id)) ids.push(seat.id);
1545
+ }
1546
+ return this.selectMany(ids);
1547
+ }
1548
+ /** Selectable seats in a section OR zone id — pure read (no selection change). */
1549
+ getSelectableInSection(sectionId) {
1550
+ const out = [];
1551
+ const seen = /* @__PURE__ */ new Set();
1552
+ for (const sec of this.sections) {
1553
+ if (sec.id !== sectionId && sec.zone !== sectionId) continue;
1554
+ for (const id of sec.memberIds) {
1555
+ if (seen.has(id)) continue;
1556
+ seen.add(id);
1557
+ if (!this.isSelectable(id)) continue;
1558
+ const s = this.seatById.get(id);
1559
+ if (s) out.push(s);
1560
+ }
1561
+ }
1562
+ return out;
1563
+ }
1564
+ /**
1565
+ * Union `ids` into the selection in one batched pass. Beyond MARQUEE_RING_CAP
1566
+ * total selected we skip the per-seat ring nodes (fill paint still marks the
1567
+ * seats) so a whole-arena select doesn't spawn thousands of Konva shapes.
1568
+ * Returns the full current selection.
1569
+ */
1570
+ selectMany(ids) {
1571
+ const fresh = ids.filter((id) => !this.selection.has(id));
1572
+ if (!fresh.length) return this.getSelection();
1573
+ const drawRings = this.selection.size + fresh.length <= MARQUEE_RING_CAP;
1574
+ for (const id of fresh) {
1575
+ if (drawRings) {
1576
+ this.setSelected(id, true, true);
1577
+ } else {
1578
+ this.selection.add(id);
1579
+ const c = this.circleById.get(id);
1580
+ if (c) this.paintSeat(c, id);
1581
+ }
1582
+ }
1583
+ if (!this.cached) this.seatLayer.batchDraw();
1584
+ this.overlayLayer.batchDraw();
1585
+ return this.getSelection();
1586
+ }
1587
+ // ---- manage-mode marquee gesture ------------------------------------------
1588
+ beginMarquee(clientPt) {
1589
+ const w = this.screenToWorld(clientPt);
1590
+ const s = this.stage.scaleX() || 1;
1591
+ const rect = new import_Rect.Rect({
1592
+ x: w.x,
1593
+ y: w.y,
1594
+ width: 0,
1595
+ height: 0,
1596
+ stroke: this.effSelection,
1597
+ strokeWidth: 1.5 / s,
1598
+ // world units → ~constant on-screen px under the stage scale
1599
+ dash: [5 / s, 4 / s],
1600
+ fill: "rgba(110,123,255,0.10)",
1601
+ listening: false,
1602
+ perfectDrawEnabled: false,
1603
+ shadowForStrokeEnabled: false
1604
+ });
1605
+ this.overlayLayer.add(rect);
1606
+ this.marquee = { start: w, rect };
1607
+ this.marqueeCur = w;
1608
+ this.overlayLayer.batchDraw();
1609
+ }
1610
+ updateMarquee(clientPt) {
1611
+ if (!this.marquee) return;
1612
+ const w = this.screenToWorld(clientPt);
1613
+ const { start, rect } = this.marquee;
1614
+ rect.setAttrs({
1615
+ x: Math.min(start.x, w.x),
1616
+ y: Math.min(start.y, w.y),
1617
+ width: Math.abs(w.x - start.x),
1618
+ height: Math.abs(w.y - start.y)
1619
+ });
1620
+ this.marqueeCur = w;
1621
+ this.overlayLayer.batchDraw();
1622
+ }
1623
+ cancelMarquee() {
1624
+ if (!this.marquee) return;
1625
+ this.marquee.rect.destroy();
1626
+ this.marquee = null;
1627
+ this.marqueeCur = null;
1628
+ this.overlayLayer.batchDraw();
1629
+ }
1630
+ /**
1631
+ * Pointer-up: hit-test the marquee world-rect against the in-memory seat
1632
+ * centres (NOT the Konva hit-graph — that's a cached bitmap when zoomed and
1633
+ * far slower), keep only SELECTABLE seats, union them into the selection and
1634
+ * fire `onMarquee`. A near-zero drag reads as a click: clear the selection.
1635
+ */
1636
+ finishMarquee() {
1637
+ const m = this.marquee;
1638
+ this.marquee = null;
1639
+ if (!m) return;
1640
+ m.rect.destroy();
1641
+ this.overlayLayer.batchDraw();
1642
+ const cur = this.marqueeCur ?? m.start;
1643
+ this.marqueeCur = null;
1644
+ const x0 = Math.min(m.start.x, cur.x);
1645
+ const x1 = Math.max(m.start.x, cur.x);
1646
+ const y0 = Math.min(m.start.y, cur.y);
1647
+ const y1 = Math.max(m.start.y, cur.y);
1648
+ const s = this.stage.scaleX() || 1;
1649
+ if ((x1 - x0) * s < 4 && (y1 - y0) * s < 4) {
1650
+ if (this.selection.size) {
1651
+ this.clearSelection();
1652
+ this.opts.onMarquee?.([]);
1653
+ }
1654
+ return;
1655
+ }
1656
+ const ids = [];
1657
+ for (const seat of this.seats) {
1658
+ if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
1659
+ if (!this.isSelectable(seat.id)) continue;
1660
+ ids.push(seat.id);
1661
+ }
1662
+ this.selectMany(ids);
1663
+ this.opts.onMarquee?.(this.getSelection());
1664
+ }
1490
1665
  flashSeat(seatId, color = "#f43f5e") {
1491
1666
  const seat = this.seatById.get(seatId);
1492
1667
  if (!seat) return;
@@ -3168,14 +3343,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3168
3343
  this.seatLayer.batchDraw();
3169
3344
  }
3170
3345
  }
3171
- cacheSeatLayer() {
3346
+ /** Rebuild the seat-layer bitmap synchronously (no paint). Shared by the
3347
+ * debounced cacheSeatLayer() and the synchronous forceDraw() catch-up. */
3348
+ rebuildSeatCache() {
3172
3349
  const pr = clamp(this.stage.scaleX() * this.dpr, 0.15, 2);
3173
3350
  this.seatLayer.clearCache();
3174
3351
  this.seatLayer.cache({ pixelRatio: pr });
3175
3352
  this.seatLayer.listening(false);
3176
3353
  this.cached = true;
3354
+ }
3355
+ cacheSeatLayer() {
3356
+ this.rebuildSeatCache();
3177
3357
  this.seatLayer.batchDraw();
3178
3358
  }
3359
+ /**
3360
+ * SYNCHRONOUS repaint that bypasses requestAnimationFrame — see the
3361
+ * ISeatmapRenderer.forceDraw() contract. Chrome pauses rAF (and therefore
3362
+ * Konva's batchDraw) on hidden/backgrounded/occluded tabs, so seat-status
3363
+ * deltas applied via setStatus() mutate the scene graph but never reach the
3364
+ * canvas until the tab is foregrounded. This flushes the cache-debounce and
3365
+ * paints every layer setStatus() can touch, immediately.
3366
+ *
3367
+ * Additive: the foreground path never calls this, so foreground behaviour is
3368
+ * byte-identical.
3369
+ */
3370
+ forceDraw() {
3371
+ if (this.recacheTimer) {
3372
+ clearTimeout(this.recacheTimer);
3373
+ this.recacheTimer = null;
3374
+ this.rebuildSeatCache();
3375
+ }
3376
+ this.bgLayer.draw();
3377
+ this.seatLayer.draw();
3378
+ this.overlayLayer.draw();
3379
+ }
3179
3380
  updateLabels() {
3180
3381
  const show = this.effScale() > LABEL_SCALE;
3181
3382
  this.labelGroup.destroyChildren();
@@ -3311,6 +3512,10 @@ var PickerController = class {
3311
3512
  this.reconnectTimer = null;
3312
3513
  this.attempt = 0;
3313
3514
  this.closed = false;
3515
+ /** Bound visibilitychange listener (buyer catch-up on tab regain). Kept on the
3516
+ * instance so destroy() can detach it; null when not attached (guards double
3517
+ * mounts / non-DOM environments). */
3518
+ this.onVisibilityChange = null;
3314
3519
  // hold state
3315
3520
  this.hold_ = null;
3316
3521
  this.liveStatuses = /* @__PURE__ */ new Map();
@@ -3436,6 +3641,7 @@ var PickerController = class {
3436
3641
  this.closedSections = new Set(closedIds);
3437
3642
  if (closedIds.length) renderer.setClosedSections?.(closedIds);
3438
3643
  if (seats) this.applySeatsMap(seats);
3644
+ this.attachVisibilityListener();
3439
3645
  this.connect();
3440
3646
  return {
3441
3647
  doc: res.doc,
@@ -3862,6 +4068,7 @@ var PickerController = class {
3862
4068
  }
3863
4069
  destroy() {
3864
4070
  this.closed = true;
4071
+ this.detachVisibilityListener();
3865
4072
  if (this.reconnectTimer) {
3866
4073
  clearTimeout(this.reconnectTimer);
3867
4074
  this.reconnectTimer = null;
@@ -4058,6 +4265,37 @@ var PickerController = class {
4058
4265
  }
4059
4266
  }
4060
4267
  // ---- realtime socket ------------------------------------------------------
4268
+ /**
4269
+ * Buyer catch-up on tab regain. While a tab is hidden Chrome pauses rAF, so
4270
+ * seat-status deltas that arrived over the WS mutated the scene graph but the
4271
+ * canvas colors never repainted. When the tab becomes visible again we (1)
4272
+ * resnapshot to pull authoritative state for anything the socket may have
4273
+ * missed while backgrounded, then (2) forceDraw() a synchronous repaint so the
4274
+ * seat colors are correct immediately rather than on the next incidental draw.
4275
+ *
4276
+ * Attached once per mount; guarded against double-attach and non-DOM
4277
+ * environments; detached in destroy() (no leaks).
4278
+ */
4279
+ attachVisibilityListener() {
4280
+ if (this.onVisibilityChange) return;
4281
+ if (typeof document === "undefined" || typeof document.addEventListener !== "function") return;
4282
+ const handler = () => {
4283
+ if (this.closed) return;
4284
+ if (document.visibilityState !== "visible") return;
4285
+ void this.resnapshot().then(() => {
4286
+ if (!this.closed) this.renderer?.forceDraw();
4287
+ });
4288
+ };
4289
+ this.onVisibilityChange = handler;
4290
+ document.addEventListener("visibilitychange", handler);
4291
+ }
4292
+ detachVisibilityListener() {
4293
+ if (!this.onVisibilityChange) return;
4294
+ if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
4295
+ document.removeEventListener("visibilitychange", this.onVisibilityChange);
4296
+ }
4297
+ this.onVisibilityChange = null;
4298
+ }
4061
4299
  connect() {
4062
4300
  if (this.closed) return;
4063
4301
  let ws;
@@ -4103,6 +4341,9 @@ var PickerController = class {
4103
4341
  }
4104
4342
  r.setStatus([id], next);
4105
4343
  }
4344
+ if (this.opts.keepLiveWhileHidden && typeof document !== "undefined" && document.visibilityState === "hidden") {
4345
+ r.forceDraw();
4346
+ }
4106
4347
  this.clearBookedHoldIfSettled();
4107
4348
  this.opts.onStatusChange?.();
4108
4349
  }