@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.js CHANGED
@@ -637,6 +637,7 @@ var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
637
637
  var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
638
638
  var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
639
639
  var MAX_LABELS = 700;
640
+ var MARQUEE_RING_CAP = 2500;
640
641
  var ISO_ANGLE_DEG = -11.5;
641
642
  var ISO_SQUASH = 0.58;
642
643
  var LIFT_PER_STEP = 58;
@@ -863,8 +864,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
863
864
  this.panLast = null;
864
865
  /** Cumulative gesture movement in px — clicks are suppressed after a real pan/pinch. */
865
866
  this.moved = 0;
867
+ /**
868
+ * Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
869
+ * points (overlayLayer rides the stage transform); `rect` is the on-canvas
870
+ * selection band. Null except during an active manage-mode drag.
871
+ */
872
+ this.marquee = null;
873
+ this.marqueeCur = null;
866
874
  // ---- keyboard navigation (accessibility) ----------------------------------
867
875
  this.onKeyDown = (e) => {
876
+ if (this.opts.manageMode) {
877
+ if ((e.metaKey || e.ctrlKey) && (e.key === "a" || e.key === "A")) {
878
+ e.preventDefault();
879
+ this.opts.onMarquee?.(this.selectAllSelectable());
880
+ return;
881
+ }
882
+ if (e.key === "Escape" && this.selection.size) {
883
+ e.preventDefault();
884
+ this.clearSelection();
885
+ this.opts.onMarquee?.([]);
886
+ return;
887
+ }
888
+ }
868
889
  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;
869
890
  if (dir) {
870
891
  e.preventDefault();
@@ -887,9 +908,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
887
908
  this.pointers.set(e.pointerId, this.toLocal(e));
888
909
  if (this.pointers.size === 1) {
889
910
  this.moved = 0;
890
- this.panLast = this.toLocal(e);
891
911
  this.pinch = null;
912
+ if (this.opts.manageMode && this.opts.marqueeSelect && e.pointerType !== "touch" && e.button === 0 && this.getRung() === "seats") {
913
+ this.beginMarquee(this.toLocal(e));
914
+ this.panLast = null;
915
+ } else {
916
+ this.panLast = this.toLocal(e);
917
+ }
892
918
  } else if (this.pointers.size === 2) {
919
+ if (this.marquee) this.cancelMarquee();
893
920
  const [a, b] = [...this.pointers.values()];
894
921
  const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
895
922
  const s = this.stage.scaleX();
@@ -908,6 +935,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
908
935
  const prev = this.pointers.get(e.pointerId);
909
936
  this.moved += Math.hypot(p.x - prev.x, p.y - prev.y);
910
937
  this.pointers.set(e.pointerId, p);
938
+ if (this.marquee) {
939
+ this.updateMarquee(p);
940
+ return;
941
+ }
911
942
  if (this.pinch && this.pointers.size >= 2) {
912
943
  const [a, b] = [...this.pointers.values()];
913
944
  const dist = Math.hypot(b.x - a.x, b.y - a.y);
@@ -933,6 +964,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
933
964
  }
934
965
  };
935
966
  this.onPointerEnd = (e) => {
967
+ if (this.marquee) {
968
+ this.finishMarquee();
969
+ this.pointers.delete(e.pointerId);
970
+ if (this.pointers.size === 0) this.panLast = null;
971
+ return;
972
+ }
936
973
  this.pointers.delete(e.pointerId);
937
974
  if (this.pointers.size < 2) this.pinch = null;
938
975
  if (this.pointers.size === 1) {
@@ -1168,6 +1205,144 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1168
1205
  }
1169
1206
  if (changed) this.overlayLayer.batchDraw();
1170
1207
  }
1208
+ // ---- manage-mode bulk selection (SDK SeatManager) -------------------------
1209
+ // All option-gated: no-ops (or empty) unless `manageMode` is set, so buyer
1210
+ // surfaces that never pass the flag can't reach any of this.
1211
+ /** Select every selectable seat on the chart (⌘A). Returns the added seats. */
1212
+ selectAllSelectable() {
1213
+ if (!this.opts.manageMode) return [];
1214
+ const ids = [];
1215
+ for (const seat of this.seats) if (this.isSelectable(seat.id)) ids.push(seat.id);
1216
+ return this.selectMany(ids);
1217
+ }
1218
+ /** Select the selectable seats matching these public labels (category/row/
1219
+ * section bulk resolves to labels host-side). Returns the newly added seats. */
1220
+ selectByLabels(labels) {
1221
+ if (!this.opts.manageMode) return [];
1222
+ const want = new Set(labels);
1223
+ const ids = [];
1224
+ for (const seat of this.seats) {
1225
+ if (want.has(seat.label) && this.isSelectable(seat.id)) ids.push(seat.id);
1226
+ }
1227
+ return this.selectMany(ids);
1228
+ }
1229
+ /** Selectable seats in a section OR zone id — pure read (no selection change). */
1230
+ getSelectableInSection(sectionId) {
1231
+ const out = [];
1232
+ const seen = /* @__PURE__ */ new Set();
1233
+ for (const sec of this.sections) {
1234
+ if (sec.id !== sectionId && sec.zone !== sectionId) continue;
1235
+ for (const id of sec.memberIds) {
1236
+ if (seen.has(id)) continue;
1237
+ seen.add(id);
1238
+ if (!this.isSelectable(id)) continue;
1239
+ const s = this.seatById.get(id);
1240
+ if (s) out.push(s);
1241
+ }
1242
+ }
1243
+ return out;
1244
+ }
1245
+ /**
1246
+ * Union `ids` into the selection in one batched pass. Beyond MARQUEE_RING_CAP
1247
+ * total selected we skip the per-seat ring nodes (fill paint still marks the
1248
+ * seats) so a whole-arena select doesn't spawn thousands of Konva shapes.
1249
+ * Returns the full current selection.
1250
+ */
1251
+ selectMany(ids) {
1252
+ const fresh = ids.filter((id) => !this.selection.has(id));
1253
+ if (!fresh.length) return this.getSelection();
1254
+ const drawRings = this.selection.size + fresh.length <= MARQUEE_RING_CAP;
1255
+ for (const id of fresh) {
1256
+ if (drawRings) {
1257
+ this.setSelected(id, true, true);
1258
+ } else {
1259
+ this.selection.add(id);
1260
+ const c = this.circleById.get(id);
1261
+ if (c) this.paintSeat(c, id);
1262
+ }
1263
+ }
1264
+ if (!this.cached) this.seatLayer.batchDraw();
1265
+ this.overlayLayer.batchDraw();
1266
+ return this.getSelection();
1267
+ }
1268
+ // ---- manage-mode marquee gesture ------------------------------------------
1269
+ beginMarquee(clientPt) {
1270
+ const w = this.screenToWorld(clientPt);
1271
+ const s = this.stage.scaleX() || 1;
1272
+ const rect = new Rect({
1273
+ x: w.x,
1274
+ y: w.y,
1275
+ width: 0,
1276
+ height: 0,
1277
+ stroke: this.effSelection,
1278
+ strokeWidth: 1.5 / s,
1279
+ // world units → ~constant on-screen px under the stage scale
1280
+ dash: [5 / s, 4 / s],
1281
+ fill: "rgba(110,123,255,0.10)",
1282
+ listening: false,
1283
+ perfectDrawEnabled: false,
1284
+ shadowForStrokeEnabled: false
1285
+ });
1286
+ this.overlayLayer.add(rect);
1287
+ this.marquee = { start: w, rect };
1288
+ this.marqueeCur = w;
1289
+ this.overlayLayer.batchDraw();
1290
+ }
1291
+ updateMarquee(clientPt) {
1292
+ if (!this.marquee) return;
1293
+ const w = this.screenToWorld(clientPt);
1294
+ const { start, rect } = this.marquee;
1295
+ rect.setAttrs({
1296
+ x: Math.min(start.x, w.x),
1297
+ y: Math.min(start.y, w.y),
1298
+ width: Math.abs(w.x - start.x),
1299
+ height: Math.abs(w.y - start.y)
1300
+ });
1301
+ this.marqueeCur = w;
1302
+ this.overlayLayer.batchDraw();
1303
+ }
1304
+ cancelMarquee() {
1305
+ if (!this.marquee) return;
1306
+ this.marquee.rect.destroy();
1307
+ this.marquee = null;
1308
+ this.marqueeCur = null;
1309
+ this.overlayLayer.batchDraw();
1310
+ }
1311
+ /**
1312
+ * Pointer-up: hit-test the marquee world-rect against the in-memory seat
1313
+ * centres (NOT the Konva hit-graph — that's a cached bitmap when zoomed and
1314
+ * far slower), keep only SELECTABLE seats, union them into the selection and
1315
+ * fire `onMarquee`. A near-zero drag reads as a click: clear the selection.
1316
+ */
1317
+ finishMarquee() {
1318
+ const m = this.marquee;
1319
+ this.marquee = null;
1320
+ if (!m) return;
1321
+ m.rect.destroy();
1322
+ this.overlayLayer.batchDraw();
1323
+ const cur = this.marqueeCur ?? m.start;
1324
+ this.marqueeCur = null;
1325
+ const x0 = Math.min(m.start.x, cur.x);
1326
+ const x1 = Math.max(m.start.x, cur.x);
1327
+ const y0 = Math.min(m.start.y, cur.y);
1328
+ const y1 = Math.max(m.start.y, cur.y);
1329
+ const s = this.stage.scaleX() || 1;
1330
+ if ((x1 - x0) * s < 4 && (y1 - y0) * s < 4) {
1331
+ if (this.selection.size) {
1332
+ this.clearSelection();
1333
+ this.opts.onMarquee?.([]);
1334
+ }
1335
+ return;
1336
+ }
1337
+ const ids = [];
1338
+ for (const seat of this.seats) {
1339
+ if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
1340
+ if (!this.isSelectable(seat.id)) continue;
1341
+ ids.push(seat.id);
1342
+ }
1343
+ this.selectMany(ids);
1344
+ this.opts.onMarquee?.(this.getSelection());
1345
+ }
1171
1346
  flashSeat(seatId, color = "#f43f5e") {
1172
1347
  const seat = this.seatById.get(seatId);
1173
1348
  if (!seat) return;
@@ -2849,14 +3024,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2849
3024
  this.seatLayer.batchDraw();
2850
3025
  }
2851
3026
  }
2852
- cacheSeatLayer() {
3027
+ /** Rebuild the seat-layer bitmap synchronously (no paint). Shared by the
3028
+ * debounced cacheSeatLayer() and the synchronous forceDraw() catch-up. */
3029
+ rebuildSeatCache() {
2853
3030
  const pr = clamp(this.stage.scaleX() * this.dpr, 0.15, 2);
2854
3031
  this.seatLayer.clearCache();
2855
3032
  this.seatLayer.cache({ pixelRatio: pr });
2856
3033
  this.seatLayer.listening(false);
2857
3034
  this.cached = true;
3035
+ }
3036
+ cacheSeatLayer() {
3037
+ this.rebuildSeatCache();
2858
3038
  this.seatLayer.batchDraw();
2859
3039
  }
3040
+ /**
3041
+ * SYNCHRONOUS repaint that bypasses requestAnimationFrame — see the
3042
+ * ISeatmapRenderer.forceDraw() contract. Chrome pauses rAF (and therefore
3043
+ * Konva's batchDraw) on hidden/backgrounded/occluded tabs, so seat-status
3044
+ * deltas applied via setStatus() mutate the scene graph but never reach the
3045
+ * canvas until the tab is foregrounded. This flushes the cache-debounce and
3046
+ * paints every layer setStatus() can touch, immediately.
3047
+ *
3048
+ * Additive: the foreground path never calls this, so foreground behaviour is
3049
+ * byte-identical.
3050
+ */
3051
+ forceDraw() {
3052
+ if (this.recacheTimer) {
3053
+ clearTimeout(this.recacheTimer);
3054
+ this.recacheTimer = null;
3055
+ this.rebuildSeatCache();
3056
+ }
3057
+ this.bgLayer.draw();
3058
+ this.seatLayer.draw();
3059
+ this.overlayLayer.draw();
3060
+ }
2860
3061
  updateLabels() {
2861
3062
  const show = this.effScale() > LABEL_SCALE;
2862
3063
  this.labelGroup.destroyChildren();
@@ -2992,6 +3193,10 @@ var PickerController = class {
2992
3193
  this.reconnectTimer = null;
2993
3194
  this.attempt = 0;
2994
3195
  this.closed = false;
3196
+ /** Bound visibilitychange listener (buyer catch-up on tab regain). Kept on the
3197
+ * instance so destroy() can detach it; null when not attached (guards double
3198
+ * mounts / non-DOM environments). */
3199
+ this.onVisibilityChange = null;
2995
3200
  // hold state
2996
3201
  this.hold_ = null;
2997
3202
  this.liveStatuses = /* @__PURE__ */ new Map();
@@ -3117,6 +3322,7 @@ var PickerController = class {
3117
3322
  this.closedSections = new Set(closedIds);
3118
3323
  if (closedIds.length) renderer.setClosedSections?.(closedIds);
3119
3324
  if (seats) this.applySeatsMap(seats);
3325
+ this.attachVisibilityListener();
3120
3326
  this.connect();
3121
3327
  return {
3122
3328
  doc: res.doc,
@@ -3543,6 +3749,7 @@ var PickerController = class {
3543
3749
  }
3544
3750
  destroy() {
3545
3751
  this.closed = true;
3752
+ this.detachVisibilityListener();
3546
3753
  if (this.reconnectTimer) {
3547
3754
  clearTimeout(this.reconnectTimer);
3548
3755
  this.reconnectTimer = null;
@@ -3739,6 +3946,37 @@ var PickerController = class {
3739
3946
  }
3740
3947
  }
3741
3948
  // ---- realtime socket ------------------------------------------------------
3949
+ /**
3950
+ * Buyer catch-up on tab regain. While a tab is hidden Chrome pauses rAF, so
3951
+ * seat-status deltas that arrived over the WS mutated the scene graph but the
3952
+ * canvas colors never repainted. When the tab becomes visible again we (1)
3953
+ * resnapshot to pull authoritative state for anything the socket may have
3954
+ * missed while backgrounded, then (2) forceDraw() a synchronous repaint so the
3955
+ * seat colors are correct immediately rather than on the next incidental draw.
3956
+ *
3957
+ * Attached once per mount; guarded against double-attach and non-DOM
3958
+ * environments; detached in destroy() (no leaks).
3959
+ */
3960
+ attachVisibilityListener() {
3961
+ if (this.onVisibilityChange) return;
3962
+ if (typeof document === "undefined" || typeof document.addEventListener !== "function") return;
3963
+ const handler = () => {
3964
+ if (this.closed) return;
3965
+ if (document.visibilityState !== "visible") return;
3966
+ void this.resnapshot().then(() => {
3967
+ if (!this.closed) this.renderer?.forceDraw();
3968
+ });
3969
+ };
3970
+ this.onVisibilityChange = handler;
3971
+ document.addEventListener("visibilitychange", handler);
3972
+ }
3973
+ detachVisibilityListener() {
3974
+ if (!this.onVisibilityChange) return;
3975
+ if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
3976
+ document.removeEventListener("visibilitychange", this.onVisibilityChange);
3977
+ }
3978
+ this.onVisibilityChange = null;
3979
+ }
3742
3980
  connect() {
3743
3981
  if (this.closed) return;
3744
3982
  let ws;
@@ -3784,6 +4022,9 @@ var PickerController = class {
3784
4022
  }
3785
4023
  r.setStatus([id], next);
3786
4024
  }
4025
+ if (this.opts.keepLiveWhileHidden && typeof document !== "undefined" && document.visibilityState === "hidden") {
4026
+ r.forceDraw();
4027
+ }
3787
4028
  this.clearBookedHoldIfSettled();
3788
4029
  this.opts.onStatusChange?.();
3789
4030
  }