@seatlayer/core 0.10.2 → 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.d.cts CHANGED
@@ -383,6 +383,13 @@ interface RendererCallbacks {
383
383
  onDeckTap?: (floorId: string) => void;
384
384
  /** Fired after any pan/zoom/resize settles — re-anchor screen-space overlays. */
385
385
  onViewChange?: () => void;
386
+ /**
387
+ * Organizer manage-mode only (`manageMode` + `marqueeSelect`): fired on
388
+ * pointer-UP after a rubber-band marquee drag, or a ⌘A/Escape bulk shortcut,
389
+ * with the FULL current selection (selectable seats only). The host toolbar
390
+ * reads this to drive bulk block/unblock. Never fires when manageMode is off.
391
+ */
392
+ onMarquee?: (seats: ExpandedSeat[]) => void;
386
393
  }
387
394
  /** Far-zoom level-of-detail rung: whole zones → section blocks → individual seats. */
388
395
  type LodRung = 'zones' | 'sections' | 'seats';
@@ -402,6 +409,21 @@ interface RendererOptions extends RendererCallbacks {
402
409
  /** ISO 4217 currency for on-map prices ("FROM …"); defaults to money.DEFAULT_CURRENCY.
403
410
  * Locale for grouping/symbol placement comes from the active i18n locale. */
404
411
  currency?: string;
412
+ /**
413
+ * Organizer manage surface (SDK SeatManager). Opt-in — enables the manage-mode
414
+ * gestures (marquee, ⌘A/Escape) and the bulk-selection helpers. Buyer pan /
415
+ * pinch / tap and every existing code path are byte-identical when this is
416
+ * false (every manage branch is gated on it). Default false.
417
+ */
418
+ manageMode?: boolean;
419
+ /**
420
+ * When `manageMode` is on, a mouse/pen primary-button drag at the seats rung
421
+ * draws a rubber-band marquee that bulk-selects the seats it covers (emitting
422
+ * `onMarquee` on pointer-up) instead of panning. Touch keeps single-finger
423
+ * pan (pinch to zoom); a middle-button drag pans with a mouse. Disabled below
424
+ * the seats rung (zoom in first). No effect unless `manageMode` is also set.
425
+ */
426
+ marqueeSelect?: boolean;
405
427
  }
406
428
  interface ISeatmapRenderer {
407
429
  /** Replace the chart. Resets selection and statuses, zooms to fit.
@@ -426,6 +448,17 @@ interface ISeatmapRenderer {
426
448
  getStatus(seatId: string): SeatStatus;
427
449
  getSelection(): ExpandedSeat[];
428
450
  clearSelection(): void;
451
+ /**
452
+ * Manage-mode bulk selection helpers (no-op / empty unless `manageMode`).
453
+ * They select the matching SELECTABLE seats (respecting `selectableStatuses`
454
+ * + closed sections), union with the current selection, and return the seats
455
+ * they added — the SDK SeatManager expands category/row/section picks to
456
+ * labels and drives one batched block/unblock from them.
457
+ */
458
+ selectAllSelectable?(): ExpandedSeat[];
459
+ selectByLabels?(labels: string[]): ExpandedSeat[];
460
+ /** Selectable seats belonging to a section OR zone id (no selection side-effect). */
461
+ getSelectableInSection?(sectionId: string): ExpandedSeat[];
429
462
  /** Programmatic deselect of specific seats (e.g. chip × in the cart). */
430
463
  deselect(seatIds: string[]): void;
431
464
  /**
@@ -806,6 +839,13 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
806
839
  private panLast;
807
840
  /** Cumulative gesture movement in px — clicks are suppressed after a real pan/pinch. */
808
841
  private moved;
842
+ /**
843
+ * Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
844
+ * points (overlayLayer rides the stage transform); `rect` is the on-canvas
845
+ * selection band. Null except during an active manage-mode drag.
846
+ */
847
+ private marquee;
848
+ private marqueeCur;
809
849
  constructor(container: HTMLDivElement, options?: RendererOptions);
810
850
  setChart(doc: ChartDoc, opts?: {
811
851
  floorId?: string;
@@ -830,6 +870,30 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
830
870
  getSelection(): ExpandedSeat[];
831
871
  clearSelection(): void;
832
872
  deselect(seatIds: string[]): void;
873
+ /** Select every selectable seat on the chart (⌘A). Returns the added seats. */
874
+ selectAllSelectable(): ExpandedSeat[];
875
+ /** Select the selectable seats matching these public labels (category/row/
876
+ * section bulk resolves to labels host-side). Returns the newly added seats. */
877
+ selectByLabels(labels: string[]): ExpandedSeat[];
878
+ /** Selectable seats in a section OR zone id — pure read (no selection change). */
879
+ getSelectableInSection(sectionId: string): ExpandedSeat[];
880
+ /**
881
+ * Union `ids` into the selection in one batched pass. Beyond MARQUEE_RING_CAP
882
+ * total selected we skip the per-seat ring nodes (fill paint still marks the
883
+ * seats) so a whole-arena select doesn't spawn thousands of Konva shapes.
884
+ * Returns the full current selection.
885
+ */
886
+ private selectMany;
887
+ private beginMarquee;
888
+ private updateMarquee;
889
+ private cancelMarquee;
890
+ /**
891
+ * Pointer-up: hit-test the marquee world-rect against the in-memory seat
892
+ * centres (NOT the Konva hit-graph — that's a cached bitmap when zoomed and
893
+ * far slower), keep only SELECTABLE seats, union them into the selection and
894
+ * fire `onMarquee`. A near-zero drag reads as a click: clear the selection.
895
+ */
896
+ private finishMarquee;
833
897
  flashSeat(seatId: string, color?: string): void;
834
898
  private onKeyDown;
835
899
  /** Nearest seat from `fromId` in a cardinal direction (aligned + close wins). */
package/dist/index.d.ts CHANGED
@@ -383,6 +383,13 @@ interface RendererCallbacks {
383
383
  onDeckTap?: (floorId: string) => void;
384
384
  /** Fired after any pan/zoom/resize settles — re-anchor screen-space overlays. */
385
385
  onViewChange?: () => void;
386
+ /**
387
+ * Organizer manage-mode only (`manageMode` + `marqueeSelect`): fired on
388
+ * pointer-UP after a rubber-band marquee drag, or a ⌘A/Escape bulk shortcut,
389
+ * with the FULL current selection (selectable seats only). The host toolbar
390
+ * reads this to drive bulk block/unblock. Never fires when manageMode is off.
391
+ */
392
+ onMarquee?: (seats: ExpandedSeat[]) => void;
386
393
  }
387
394
  /** Far-zoom level-of-detail rung: whole zones → section blocks → individual seats. */
388
395
  type LodRung = 'zones' | 'sections' | 'seats';
@@ -402,6 +409,21 @@ interface RendererOptions extends RendererCallbacks {
402
409
  /** ISO 4217 currency for on-map prices ("FROM …"); defaults to money.DEFAULT_CURRENCY.
403
410
  * Locale for grouping/symbol placement comes from the active i18n locale. */
404
411
  currency?: string;
412
+ /**
413
+ * Organizer manage surface (SDK SeatManager). Opt-in — enables the manage-mode
414
+ * gestures (marquee, ⌘A/Escape) and the bulk-selection helpers. Buyer pan /
415
+ * pinch / tap and every existing code path are byte-identical when this is
416
+ * false (every manage branch is gated on it). Default false.
417
+ */
418
+ manageMode?: boolean;
419
+ /**
420
+ * When `manageMode` is on, a mouse/pen primary-button drag at the seats rung
421
+ * draws a rubber-band marquee that bulk-selects the seats it covers (emitting
422
+ * `onMarquee` on pointer-up) instead of panning. Touch keeps single-finger
423
+ * pan (pinch to zoom); a middle-button drag pans with a mouse. Disabled below
424
+ * the seats rung (zoom in first). No effect unless `manageMode` is also set.
425
+ */
426
+ marqueeSelect?: boolean;
405
427
  }
406
428
  interface ISeatmapRenderer {
407
429
  /** Replace the chart. Resets selection and statuses, zooms to fit.
@@ -426,6 +448,17 @@ interface ISeatmapRenderer {
426
448
  getStatus(seatId: string): SeatStatus;
427
449
  getSelection(): ExpandedSeat[];
428
450
  clearSelection(): void;
451
+ /**
452
+ * Manage-mode bulk selection helpers (no-op / empty unless `manageMode`).
453
+ * They select the matching SELECTABLE seats (respecting `selectableStatuses`
454
+ * + closed sections), union with the current selection, and return the seats
455
+ * they added — the SDK SeatManager expands category/row/section picks to
456
+ * labels and drives one batched block/unblock from them.
457
+ */
458
+ selectAllSelectable?(): ExpandedSeat[];
459
+ selectByLabels?(labels: string[]): ExpandedSeat[];
460
+ /** Selectable seats belonging to a section OR zone id (no selection side-effect). */
461
+ getSelectableInSection?(sectionId: string): ExpandedSeat[];
429
462
  /** Programmatic deselect of specific seats (e.g. chip × in the cart). */
430
463
  deselect(seatIds: string[]): void;
431
464
  /**
@@ -806,6 +839,13 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
806
839
  private panLast;
807
840
  /** Cumulative gesture movement in px — clicks are suppressed after a real pan/pinch. */
808
841
  private moved;
842
+ /**
843
+ * Manage-mode rubber-band marquee (option-gated). `start`/`cur` are WORLD-space
844
+ * points (overlayLayer rides the stage transform); `rect` is the on-canvas
845
+ * selection band. Null except during an active manage-mode drag.
846
+ */
847
+ private marquee;
848
+ private marqueeCur;
809
849
  constructor(container: HTMLDivElement, options?: RendererOptions);
810
850
  setChart(doc: ChartDoc, opts?: {
811
851
  floorId?: string;
@@ -830,6 +870,30 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
830
870
  getSelection(): ExpandedSeat[];
831
871
  clearSelection(): void;
832
872
  deselect(seatIds: string[]): void;
873
+ /** Select every selectable seat on the chart (⌘A). Returns the added seats. */
874
+ selectAllSelectable(): ExpandedSeat[];
875
+ /** Select the selectable seats matching these public labels (category/row/
876
+ * section bulk resolves to labels host-side). Returns the newly added seats. */
877
+ selectByLabels(labels: string[]): ExpandedSeat[];
878
+ /** Selectable seats in a section OR zone id — pure read (no selection change). */
879
+ getSelectableInSection(sectionId: string): ExpandedSeat[];
880
+ /**
881
+ * Union `ids` into the selection in one batched pass. Beyond MARQUEE_RING_CAP
882
+ * total selected we skip the per-seat ring nodes (fill paint still marks the
883
+ * seats) so a whole-arena select doesn't spawn thousands of Konva shapes.
884
+ * Returns the full current selection.
885
+ */
886
+ private selectMany;
887
+ private beginMarquee;
888
+ private updateMarquee;
889
+ private cancelMarquee;
890
+ /**
891
+ * Pointer-up: hit-test the marquee world-rect against the in-memory seat
892
+ * centres (NOT the Konva hit-graph — that's a cached bitmap when zoomed and
893
+ * far slower), keep only SELECTABLE seats, union them into the selection and
894
+ * fire `onMarquee`. A near-zero drag reads as a click: clear the selection.
895
+ */
896
+ private finishMarquee;
833
897
  flashSeat(seatId: string, color?: string): void;
834
898
  private onKeyDown;
835
899
  /** Nearest seat from `fromId` in a cardinal direction (aligned + close wins). */
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;