@seatlayer/core 0.51.0 → 0.52.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.
Files changed (37) hide show
  1. package/dist/{chunk-GD267BFB.js → chunk-UID7KN44.js} +75 -2
  2. package/dist/chunk-UID7KN44.js.map +1 -0
  3. package/dist/core/seatConfidence.d.cts +1 -1
  4. package/dist/core/seatConfidence.d.ts +1 -1
  5. package/dist/{de-V53JPFHN.js → de-YGODYJ3E.js} +3 -3
  6. package/dist/de-YGODYJ3E.js.map +1 -0
  7. package/dist/{es-37ISLCZA.js → es-VMBTGVFQ.js} +3 -3
  8. package/dist/es-VMBTGVFQ.js.map +1 -0
  9. package/dist/{fr-QKK2T64Q.js → fr-SLBE2ULB.js} +3 -3
  10. package/dist/fr-SLBE2ULB.js.map +1 -0
  11. package/dist/index.cjs +647 -60
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +131 -17
  14. package/dist/index.d.ts +131 -17
  15. package/dist/index.js +575 -58
  16. package/dist/index.js.map +1 -1
  17. package/dist/picker/minimapGeometry.cjs +80 -0
  18. package/dist/picker/minimapGeometry.cjs.map +1 -0
  19. package/dist/picker/minimapGeometry.d.cts +37 -0
  20. package/dist/picker/minimapGeometry.d.ts +37 -0
  21. package/dist/picker/minimapGeometry.js +52 -0
  22. package/dist/picker/minimapGeometry.js.map +1 -0
  23. package/dist/{types-DR54nqKL.d.cts → types-tLOunI-B.d.cts} +131 -1
  24. package/dist/{types-DR54nqKL.d.ts → types-tLOunI-B.d.ts} +131 -1
  25. package/dist/view3d/crossfade/panorama.d.cts +1 -1
  26. package/dist/view3d/crossfade/panorama.d.ts +1 -1
  27. package/dist/view3d/index.cjs +1158 -123
  28. package/dist/view3d/index.cjs.map +1 -1
  29. package/dist/view3d/index.d.cts +20 -12
  30. package/dist/view3d/index.d.ts +20 -12
  31. package/dist/view3d/index.js +1158 -122
  32. package/dist/view3d/index.js.map +1 -1
  33. package/package.json +14 -1
  34. package/dist/chunk-GD267BFB.js.map +0 -1
  35. package/dist/de-V53JPFHN.js.map +0 -1
  36. package/dist/es-37ISLCZA.js.map +0 -1
  37. package/dist/fr-QKK2T64Q.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -45,6 +45,8 @@ var init_es = __esm({
45
45
  "picker.poweredBy": "Con la tecnolog\xEDa de SeatLayer",
46
46
  "picker.testMode": "MODO DE PRUEBA",
47
47
  "picker.orphanHint": "Esto deja un asiento aislado \u2014 considera desplazarte un asiento.",
48
+ "picker.companionRequiresWheelchair": "Requiere la plaza accesible contigua",
49
+ "picker.companionPairRequired": "Selecciona la plaza accesible contigua con cada entrada de acompa\xF1ante.",
48
50
  // renderer (drawn on the Konva map — shared by the embed SDK)
49
51
  "map.aria": "Mapa de asientos. Usa las flechas para moverte entre asientos, Intro para seleccionar.",
50
52
  "map.seatsLeft": "{count} LIBRES",
@@ -156,8 +158,6 @@ var init_es = __esm({
156
158
  "picker.fewerGuests": "Menos invitados",
157
159
  "picker.fewerSeats": "Menos asientos",
158
160
  "picker.filterAndFocusByPrice": "Filtrar y enfocar asientos por precio",
159
- "picker.filteredSeat": "No disponible con los filtros actuales",
160
- "picker.filteredSeatExplanation": "Cambia el filtro de precio o vista activo para poder seleccionar este asiento.",
161
161
  "picker.filters": "Filtros",
162
162
  "picker.findBestSeatsCount.one": "Buscar {count} mejor asiento",
163
163
  "picker.findBestSeatsCount.other": "Buscar {count} mejores asientos",
@@ -344,6 +344,8 @@ var init_de = __esm({
344
344
  "picker.poweredBy": "Bereitgestellt von SeatLayer",
345
345
  "picker.testMode": "TESTMODUS",
346
346
  "picker.orphanHint": "Dadurch bleibt ein einzelner Platz \xFCbrig \u2014 r\xFCcken Sie ggf. einen Platz weiter.",
347
+ "picker.companionRequiresWheelchair": "Benachbarter Rollstuhlplatz erforderlich",
348
+ "picker.companionPairRequired": "W\xE4hlen Sie zu jedem Begleitticket den benachbarten Rollstuhlplatz aus.",
347
349
  // renderer (drawn on the Konva map — shared by the embed SDK)
348
350
  "map.aria": "Sitzplan. Mit Pfeiltasten zwischen Pl\xE4tzen navigieren, Eingabe zum Ausw\xE4hlen.",
349
351
  "map.seatsLeft": "{count} FREI",
@@ -455,8 +457,6 @@ var init_de = __esm({
455
457
  "picker.fewerGuests": "Weniger G\xE4ste",
456
458
  "picker.fewerSeats": "Weniger Pl\xE4tze",
457
459
  "picker.filterAndFocusByPrice": "Pl\xE4tze nach Preis filtern und fokussieren",
458
- "picker.filteredSeat": "Mit aktuellen Filtern nicht verf\xFCgbar",
459
- "picker.filteredSeatExplanation": "\xC4ndern Sie den aktiven Preis- oder Ansichtsfilter, um diesen Platz ausw\xE4hlbar zu machen.",
460
460
  "picker.filters": "Filter",
461
461
  "picker.findBestSeatsCount.one": "{count} besten Platz finden",
462
462
  "picker.findBestSeatsCount.other": "{count} beste Pl\xE4tze finden",
@@ -643,6 +643,8 @@ var init_fr = __esm({
643
643
  "picker.poweredBy": "Propuls\xE9 par SeatLayer",
644
644
  "picker.testMode": "MODE TEST",
645
645
  "picker.orphanHint": "Cela laisse un si\xE8ge isol\xE9 \u2014 pensez \xE0 vous d\xE9caler d'un si\xE8ge.",
646
+ "picker.companionRequiresWheelchair": "N\xE9cessite la place fauteuil roulant adjacente",
647
+ "picker.companionPairRequired": "S\xE9lectionnez la place fauteuil roulant adjacente avec chaque billet accompagnateur.",
646
648
  // renderer (drawn on the Konva map — shared by the embed SDK)
647
649
  "map.aria": "Plan de salle. Utilisez les fl\xE8ches pour naviguer entre les si\xE8ges, Entr\xE9e pour s\xE9lectionner.",
648
650
  "map.seatsLeft": "{count} LIBRES",
@@ -754,8 +756,6 @@ var init_fr = __esm({
754
756
  "picker.fewerGuests": "Moins d\u2019invit\xE9s",
755
757
  "picker.fewerSeats": "Moins de places",
756
758
  "picker.filterAndFocusByPrice": "Filtrer et cibler les places par prix",
757
- "picker.filteredSeat": "Indisponible avec les filtres actuels",
758
- "picker.filteredSeatExplanation": "Modifiez le filtre de prix ou de vue actif pour rendre cette place s\xE9lectionnable.",
759
759
  "picker.filters": "Filtres",
760
760
  "picker.findBestSeatsCount.one": "Trouver {count} meilleure place",
761
761
  "picker.findBestSeatsCount.other": "Trouver {count} meilleures places",
@@ -935,6 +935,7 @@ __export(index_exports, {
935
935
  MAX_EVENT_INVENTORY: () => MAX_EVENT_INVENTORY,
936
936
  MAX_GA_CAPACITY: () => MAX_GA_CAPACITY,
937
937
  PickerController: () => PickerController,
938
+ PickerSelectionError: () => PickerSelectionError,
938
939
  RENDERED_QUALITY_REPORT_VERSION: () => RENDERED_QUALITY_REPORT_VERSION,
939
940
  SEAT_COMMERCIAL_MARKS: () => SEAT_COMMERCIAL_MARKS,
940
941
  SUPPORTED_LOCALES: () => SUPPORTED_LOCALES,
@@ -958,6 +959,7 @@ __export(index_exports, {
958
959
  expandTable: () => expandTable,
959
960
  expandTableSlots: () => expandTableSlots,
960
961
  floorObjects: () => floorObjects,
962
+ floorOverviewTranslations: () => floorOverviewTranslations,
961
963
  floorsOf: () => floorsOf,
962
964
  formatDate: () => formatDate,
963
965
  formatMinor: () => formatMinor,
@@ -976,6 +978,7 @@ __export(index_exports, {
976
978
  isGaUnitLabel: () => isGaUnitLabel,
977
979
  isSectionHidden: () => isSectionHidden,
978
980
  layerOf: () => layerOf,
981
+ layoutFloorOverview: () => layoutFloorOverview,
979
982
  loadLocale: () => loadLocale,
980
983
  objectCenter: () => objectCenter,
981
984
  owningSectionForObject: () => owningSectionForObject,
@@ -995,6 +998,7 @@ __export(index_exports, {
995
998
  tCount: () => tCount,
996
999
  tableInventoryCount: () => tableInventoryCount,
997
1000
  tableSeatCountsBySide: () => tableSeatCountsBySide,
1001
+ translateObject: () => translateObject,
998
1002
  validGAInventorySegments: () => validGAInventorySegments
999
1003
  });
1000
1004
  module.exports = __toCommonJS(index_exports);
@@ -1345,7 +1349,7 @@ function resolveSegmentedRowGroups(objects) {
1345
1349
  const expectedCount = ordered[0]?.segmentedRow?.componentCount ?? 0;
1346
1350
  const first = ordered[0]?.segmentedRow;
1347
1351
  if (!first) continue;
1348
- const valid = expectedCount >= 2 && ordered.length === expectedCount && first?.boundaryBefore === "start" && ordered.every((row, index) => row.segmentedRow?.kind === "segmented-row-v1" && row.segmentedRow.groupId === groupId && row.segmentedRow.componentCount === expectedCount && row.segmentedRow.componentIndex === index && (index === 0 ? row.segmentedRow.boundaryBefore === "start" : row.segmentedRow.boundaryBefore !== "start") && row.segmentedRow.displayLabel === first.displayLabel);
1352
+ const valid = expectedCount >= 2 && ordered.length === expectedCount && first?.boundaryBefore === "start" && ordered.every((row, index) => row.segmentedRow?.kind === "segmented-row-v1" && row.segmentedRow.groupId === groupId && row.segmentedRow.componentCount === expectedCount && row.segmentedRow.componentIndex === index && row.segmentedRow.repeatLabelOnComponents === first.repeatLabelOnComponents && (index === 0 ? row.segmentedRow.boundaryBefore === "start" : row.segmentedRow.boundaryBefore !== "start") && row.segmentedRow.displayLabel === first.displayLabel);
1349
1353
  if (!valid) continue;
1350
1354
  const totalSeats = ordered.reduce((sum, row) => sum + row.seatCount, 0);
1351
1355
  let adjacencyOffset = 0;
@@ -2239,6 +2243,75 @@ function stackFloors(doc, spread = 900) {
2239
2243
  });
2240
2244
  return { ...doc, objects, floors: void 0 };
2241
2245
  }
2246
+ function layoutFloorOverview(doc) {
2247
+ const floors = doc.floors;
2248
+ if (!floors || floors.length < 2) return doc;
2249
+ const translations = floorOverviewTranslations(doc);
2250
+ const objects = [];
2251
+ floors.forEach((floor, index) => {
2252
+ const { x: dx, y: dy } = translations[index];
2253
+ for (const object of floor.objects) objects.push(translateObject(object, dx, dy));
2254
+ });
2255
+ return {
2256
+ ...doc,
2257
+ objects,
2258
+ floors: void 0,
2259
+ referenceImage: void 0,
2260
+ backgroundImage: void 0
2261
+ };
2262
+ }
2263
+ function floorOverviewTranslations(doc) {
2264
+ const floors = doc.floors;
2265
+ if (!floors?.length) return [{ x: 0, y: 0 }];
2266
+ if (floors.length < 2) return floors.map(() => ({ x: 0, y: 0 }));
2267
+ const bounds2 = floors.map((floor) => chartBounds({
2268
+ ...doc,
2269
+ objects: floor.objects,
2270
+ floors: void 0,
2271
+ focalPoint: floor.focalPoint,
2272
+ referenceImage: void 0,
2273
+ backgroundImage: void 0
2274
+ }));
2275
+ const characteristic = Math.max(1, ...bounds2.map((bound) => Math.max(bound.width, bound.height)));
2276
+ const gap = Math.max(64, Math.min(240, characteristic * 0.08));
2277
+ const targetAspect = 16 / 10;
2278
+ let best = null;
2279
+ for (let columns2 = 1; columns2 <= floors.length; columns2 += 1) {
2280
+ const rows = Math.ceil(floors.length / columns2);
2281
+ const columnWidths2 = new Array(columns2).fill(0);
2282
+ const rowHeights2 = new Array(rows).fill(0);
2283
+ bounds2.forEach((bound, index) => {
2284
+ const column = index % columns2;
2285
+ const row = Math.floor(index / columns2);
2286
+ columnWidths2[column] = Math.max(columnWidths2[column], bound.width);
2287
+ rowHeights2[row] = Math.max(rowHeights2[row], bound.height);
2288
+ });
2289
+ const width = columnWidths2.reduce((sum, value) => sum + value, 0) + gap * (columns2 - 1);
2290
+ const height = rowHeights2.reduce((sum, value) => sum + value, 0) + gap * (rows - 1);
2291
+ const unusedCells = columns2 * rows - floors.length;
2292
+ const score = Math.abs(Math.log(width / Math.max(1, height) / targetAspect)) + unusedCells * 0.04;
2293
+ if (!best || score < best.score) best = { columns: columns2, columnWidths: columnWidths2, rowHeights: rowHeights2, score };
2294
+ }
2295
+ const { columns, columnWidths, rowHeights } = best;
2296
+ const columnOffsets = [];
2297
+ const rowOffsets = [];
2298
+ columnWidths.reduce((offset, width, index) => {
2299
+ columnOffsets[index] = offset;
2300
+ return offset + width + gap;
2301
+ }, 0);
2302
+ rowHeights.reduce((offset, height, index) => {
2303
+ rowOffsets[index] = offset;
2304
+ return offset + height + gap;
2305
+ }, 0);
2306
+ return floors.map((_floor, index) => {
2307
+ const bound = bounds2[index];
2308
+ const column = index % columns;
2309
+ const row = Math.floor(index / columns);
2310
+ const targetX = columnOffsets[column] + (columnWidths[column] - bound.width) / 2;
2311
+ const targetY = rowOffsets[row] + (rowHeights[row] - bound.height) / 2;
2312
+ return { x: targetX - bound.x, y: targetY - bound.y };
2313
+ });
2314
+ }
2242
2315
  function expandFloorObjects(objects, zones, fallbackFocal, activeConfigurationId, viewFallback, confidenceFallback) {
2243
2316
  const out = [];
2244
2317
  const segmented = resolveSegmentedRowGroups(objects);
@@ -3697,6 +3770,8 @@ var en = {
3697
3770
  "picker.poweredBy": "Powered by SeatLayer",
3698
3771
  "picker.testMode": "TEST MODE",
3699
3772
  "picker.orphanHint": "This leaves a single seat stranded \u2014 consider shifting one seat over.",
3773
+ "picker.companionRequiresWheelchair": "Requires the adjacent wheelchair place",
3774
+ "picker.companionPairRequired": "Select the adjacent wheelchair place with each companion ticket.",
3700
3775
  // renderer (drawn on the Konva map — shared by the embed SDK)
3701
3776
  "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
3702
3777
  "map.seatsLeft": "{count} LEFT",
@@ -3808,8 +3883,6 @@ var en = {
3808
3883
  "picker.fewerGuests": "Fewer guests",
3809
3884
  "picker.fewerSeats": "Fewer seats",
3810
3885
  "picker.filterAndFocusByPrice": "Filter and focus seats by price",
3811
- "picker.filteredSeat": "Unavailable with current filters",
3812
- "picker.filteredSeatExplanation": "Change the active price or view filters to make this seat selectable.",
3813
3886
  "picker.filters": "Filters",
3814
3887
  "picker.findBestSeatsCount.one": "Find {count} best seat",
3815
3888
  "picker.findBestSeatsCount.other": "Find {count} best seats",
@@ -4045,10 +4118,11 @@ var WHEELCHAIR_GLYPH_MIN_PX = 10;
4045
4118
  var FILTERED_WHEELCHAIR_GLYPH_MIN_PX = 14;
4046
4119
  var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
4047
4120
  var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
4048
- var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
4121
+ var SEAT_DETAIL_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
4049
4122
  var PAN_START_SLOP_PX = 8;
4050
4123
  var GHOST_CLICK_MS = 700;
4051
4124
  var ROW_LABEL_CLEARANCE = 2;
4125
+ var FOCUSED_ROW_LABEL_MIN_PX = MIN_VISIBLE_BOOKABLE_LABEL_PX;
4052
4126
  var ROW_LABEL_COLLISION_CELL = 64;
4053
4127
  var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
4054
4128
  var MAX_LABELS = 700;
@@ -4444,6 +4518,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4444
4518
  this.activeFloorId = "";
4445
4519
  /** When true on a multi-floor chart, render ALL floors stacked (3D overview). */
4446
4520
  this.stacked = false;
4521
+ /** Buyer-facing 2D overview: floors are rigidly arranged without altering topology. */
4522
+ this.floorOverview = false;
4447
4523
  /** Interactive node per seat/booth — a Circle for seats, a Rect for booths. */
4448
4524
  this.circleById = /* @__PURE__ */ new Map();
4449
4525
  /** Booth block geometry, keyed by booth id (= the unit's rowId). */
@@ -4699,6 +4775,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4699
4775
  return;
4700
4776
  }
4701
4777
  if (this.pinch && this.pointers.size >= 2) {
4778
+ this.releaseSectionFocusDim();
4702
4779
  const [a, b] = [...this.pointers.values()];
4703
4780
  const dist = Math.hypot(b.x - a.x, b.y - a.y);
4704
4781
  if (this.pinch.startDist < 1) return;
@@ -4717,6 +4794,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4717
4794
  if (!this.panStarted) {
4718
4795
  if (this.moved <= PAN_START_SLOP_PX) return;
4719
4796
  this.panStarted = true;
4797
+ this.releaseSectionFocusDim();
4720
4798
  }
4721
4799
  this.stage.position({
4722
4800
  x: this.stage.x() + (p.x - this.panLast.x),
@@ -4837,13 +4915,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4837
4915
  }
4838
4916
  // ---- ISeatmapRenderer -----------------------------------------------------
4839
4917
  setChart(doc, opts) {
4840
- if (doc !== this.chartDoc) this.stacked = false;
4918
+ if (doc !== this.chartDoc) {
4919
+ this.stacked = false;
4920
+ this.floorOverview = false;
4921
+ }
4841
4922
  this.cancelAssetLoads();
4842
4923
  this.chartDoc = doc;
4843
4924
  this.activeFloorId = opts?.floorId ?? floorsOf(doc)[0].id;
4844
4925
  this.objectFloor.clear();
4845
4926
  if (doc.floors && doc.floors.length > 1) {
4846
- for (const f of doc.floors) for (const o of f.objects) this.objectFloor.set(o.id, f.id);
4927
+ for (const f of doc.floors) {
4928
+ for (const o of f.objects) {
4929
+ this.objectFloor.set(o.id, f.id);
4930
+ if (o.type === "section" && o.logicalSectionId) {
4931
+ this.objectFloor.set(o.logicalSectionId, f.id);
4932
+ }
4933
+ }
4934
+ }
4847
4935
  }
4848
4936
  const view = this.floorView(doc);
4849
4937
  this.focusedId = null;
@@ -4941,7 +5029,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4941
5029
  this.seats = expandChart(view, {
4942
5030
  // A stacked overview is presentation-only and never opens a seat POV. The
4943
5031
  // per-section renderer below still resolves each object's real floor base.
4944
- floorBaseHeightM: this.stacked ? 0 : this.floorBaseHeightMFor()
5032
+ floorBaseHeightM: this.stacked || this.floorOverview ? 0 : this.floorBaseHeightMFor()
4945
5033
  });
4946
5034
  for (const s of this.seats) {
4947
5035
  this.seatById.set(s.id, s);
@@ -4962,14 +5050,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4962
5050
  this.overlayLayer.add(this.focusRing);
4963
5051
  this.zoomToFit();
4964
5052
  }
4965
- /** The chart to render: all floors stacked (3D overview), the active floor, or
4966
- * the whole chart for single-floor charts. */
5053
+ /** The chart to render: authored-coordinate overview, stacked 3D overview,
5054
+ * the active floor, or the whole chart for single-floor charts. */
4967
5055
  floorView(doc) {
4968
5056
  if (!doc.floors || !doc.floors.length) return {
4969
5057
  ...doc,
4970
5058
  referenceImage: void 0,
4971
5059
  backgroundImage: buyerBackgroundImage(doc)
4972
5060
  };
5061
+ if (this.floorOverview && doc.floors.length >= 2) {
5062
+ return {
5063
+ ...layoutFloorOverview(doc),
5064
+ referenceImage: void 0,
5065
+ backgroundImage: buyerBackgroundImage(doc)
5066
+ };
5067
+ }
4973
5068
  if (this.stacked && doc.floors.length >= 2) {
4974
5069
  return {
4975
5070
  ...stackFloors(doc),
@@ -5002,14 +5097,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5002
5097
  const multi = !!this.chartDoc.floors && this.chartDoc.floors.length >= 2;
5003
5098
  if (!multi) return;
5004
5099
  this.stacked = on;
5100
+ if (on) this.floorOverview = false;
5005
5101
  this.setChart(this.chartDoc, { floorId: this.activeFloorId });
5006
5102
  }
5007
5103
  isStacked() {
5008
5104
  return this.stacked;
5009
5105
  }
5106
+ /** Toggle the buyer's combined 2D floor overview. Every floor is translated
5107
+ * as one rigid group for clarity; its authored internal topology is exact. */
5108
+ setFloorOverview(on) {
5109
+ if (!this.chartDoc || on === this.floorOverview) return;
5110
+ const multi = !!this.chartDoc.floors && this.chartDoc.floors.length >= 2;
5111
+ if (!multi) return;
5112
+ this.floorOverview = on;
5113
+ if (on) this.stacked = false;
5114
+ this.setChart(this.chartDoc, { floorId: this.activeFloorId });
5115
+ }
5116
+ isFloorOverview() {
5117
+ return this.floorOverview;
5118
+ }
5010
5119
  /** Switch which floor is shown (2D). Re-renders + re-fits; no-op if unchanged. */
5011
5120
  setActiveFloor(floorId) {
5012
- if (!this.chartDoc || floorId === this.activeFloorId) return;
5121
+ if (!this.chartDoc || floorId === this.activeFloorId && !this.floorOverview && !this.stacked) return;
5122
+ this.floorOverview = false;
5123
+ this.stacked = false;
5013
5124
  this.setChart(this.chartDoc, { floorId });
5014
5125
  }
5015
5126
  /** Floors for the host's switcher (single-floor charts return one synthetic floor). */
@@ -5468,6 +5579,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5468
5579
  const h = this.stage.height();
5469
5580
  const b = this.viewMode === "perspective" && this.perspectiveBounds ? this.perspectiveBounds : this.bounds;
5470
5581
  this.fitScale = Math.min(w / b.width, h / b.height) || 1;
5582
+ if (this.opts.portraitFitCrop && !this.exportMode) {
5583
+ const widthBound = w / b.width <= h / b.height;
5584
+ const projectedH = b.height * this.fitScale;
5585
+ if (widthBound && projectedH < 0.55 * h) {
5586
+ this.fitScale = Math.min(this.fitScale * 1.3, 0.62 * h / b.height);
5587
+ }
5588
+ }
5471
5589
  const s = this.fitScale;
5472
5590
  this.stage.scale({ x: s, y: s });
5473
5591
  this.stage.position({
@@ -5477,6 +5595,18 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5477
5595
  this.afterViewChange();
5478
5596
  this.stage.batchDraw();
5479
5597
  }
5598
+ /**
5599
+ * Reframe after a host/layout resize without throwing away section context.
5600
+ * The stage already represents the usable map area (the picker side panel is
5601
+ * a sibling), so measuring it here naturally respects open/closed panels.
5602
+ */
5603
+ refitCurrentView() {
5604
+ if (this.focusedSectionId && this.sectionBounds(this.focusedSectionId)) {
5605
+ this.focusRegion(this.focusedSectionId, { animate: false });
5606
+ return;
5607
+ }
5608
+ this.zoomToFit();
5609
+ }
5480
5610
  zoomIn() {
5481
5611
  const center = { x: this.stage.width() / 2, y: this.stage.height() / 2 };
5482
5612
  this.zoomAbout(this.stage.scaleX() * _SeatmapRenderer.ZOOM_STEP, center);
@@ -6567,8 +6697,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6567
6697
  }
6568
6698
  /**
6569
6699
  * AXS section-focus: dim + desaturate every other section, draw a calm backdrop
6570
- * panel behind this section's seats, and glide the camera in to frame it (the
6571
- * seat-pick gate below only lets buyers pick once seats are LABEL_SCALE big).
6700
+ * panel behind this section's seats, and glide the camera to a padded fit of
6701
+ * the whole section. A large section may therefore use smaller seat markers,
6702
+ * but it never clips its outer rows merely to meet a fixed detail scale.
6572
6703
  */
6573
6704
  focusSection(id) {
6574
6705
  if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
@@ -6577,7 +6708,24 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6577
6708
  this.bgLayer.batchDraw();
6578
6709
  this.overlayLayer.batchDraw();
6579
6710
  this.updateLOD();
6580
- this.focusRegion(id, { minScale: SEAT_FOCUS_SCALE });
6711
+ this.focusRegion(id);
6712
+ }
6713
+ /**
6714
+ * Release only the focus dim + backdrop, keeping the focused-section state
6715
+ * (summary card, adjacent-section navigation, LOD) intact. Called on the
6716
+ * buyer's first manual pan/pinch/wheel after a section glide: the dim reads
6717
+ * as "everything else is disabled", but every seat outside the focus was
6718
+ * always hit-testable — free roaming across sections is the product
6719
+ * behaviour, the glide is only an entrance.
6720
+ */
6721
+ releaseSectionFocusDim() {
6722
+ if (!this.focusBackdrop && !this.focusDimOverlay) return;
6723
+ this.focusBackdrop?.destroy();
6724
+ this.focusBackdrop = null;
6725
+ this.focusDimOverlay?.destroy();
6726
+ this.focusDimOverlay = null;
6727
+ this.bgLayer.batchDraw();
6728
+ this.overlayLayer.batchDraw();
6581
6729
  }
6582
6730
  /** Clear an AXS section focus — restore full-bowl brightness + drop the backdrop. */
6583
6731
  clearSectionFocus() {
@@ -6715,6 +6863,44 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6715
6863
  if (!Number.isFinite(minX)) return { x: 0, y: 0, width: 1, height: 1 };
6716
6864
  return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
6717
6865
  }
6866
+ /** Live minimap geometry comes from the rendered scene, so a content-aware
6867
+ * all-floor layout cannot collapse back onto the source coordinate stack. */
6868
+ getMinimapSnapshot() {
6869
+ const sections = this.sections.map((section) => {
6870
+ const inactive = this.isSectionClosed(section) || section.free <= 0;
6871
+ const selected = section.memberIds.some((id) => this.selection.has(id));
6872
+ const active2 = this.focusedSectionId === section.id || this.focusedSectionId === section.logicalId;
6873
+ return {
6874
+ id: section.logicalId,
6875
+ label: section.label,
6876
+ outline: section.outline.map((point) => ({ ...point })),
6877
+ ...this.objectFloor.get(section.id) ? { floorId: this.objectFloor.get(section.id) } : {},
6878
+ state: selected ? "selected" : inactive ? "inactive" : "available",
6879
+ active: active2
6880
+ };
6881
+ });
6882
+ return {
6883
+ sections,
6884
+ seats: sections.length ? [] : this.seats.map((seat) => ({
6885
+ x: seat.x,
6886
+ y: seat.y,
6887
+ categoryKey: seat.categoryKey,
6888
+ state: this.selection.has(seat.id) ? "selected" : this.statusById.get(seat.id) === "free" ? "available" : "inactive"
6889
+ }))
6890
+ };
6891
+ }
6892
+ /** Minimap drag/click navigation: preserve zoom and move one world point to
6893
+ * the viewport centre. Works for flat and projected views via worldToScreen. */
6894
+ panToWorld(point) {
6895
+ this.cancelGlide();
6896
+ const screen = this.worldToScreen(point);
6897
+ this.stage.position({
6898
+ x: this.stage.x() + this.stage.width() / 2 - screen.x,
6899
+ y: this.stage.y() + this.stage.height() / 2 - screen.y
6900
+ });
6901
+ this.afterViewChange();
6902
+ this.stage.batchDraw();
6903
+ }
6718
6904
  /** Legend hover: highlight one category (dim the rest), or null to clear. */
6719
6905
  setCategoryHighlight(key) {
6720
6906
  if (this.categoryHighlight === key) return;
@@ -7483,7 +7669,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7483
7669
  blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
7484
7670
  zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
7485
7671
  }
7486
- const sectionOverview = scale < CACHE_THRESHOLD;
7672
+ if (this.floorOverview) {
7673
+ blockT = 0;
7674
+ zoneT = 0;
7675
+ }
7676
+ if (this.focusedSectionId) {
7677
+ blockT = 0;
7678
+ zoneT = 0;
7679
+ }
7680
+ const sectionOverview = !this.floorOverview && !this.focusedSectionId && scale < CACHE_THRESHOLD;
7487
7681
  if (sectionOverview) blockT = 1;
7488
7682
  if (!this.zones.length) zoneT = 0;
7489
7683
  const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
@@ -7682,6 +7876,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7682
7876
  zone.back.offset({ x: width / 2, y: height / 2 });
7683
7877
  zone.back.cornerRadius(7 * this.uiScale() / sx);
7684
7878
  zone.back.strokeWidth(1 / sx);
7879
+ const abs = zone.back.getAbsolutePosition();
7880
+ const minScreenY = 62 + height * sx / 2;
7881
+ if (abs.y < minScreenY) {
7882
+ const dy = (minScreenY - abs.y) / sx;
7883
+ zone.label.y(zone.label.y() + dy);
7884
+ if (zone.sub) zone.sub.y(zone.sub.y() + dy);
7885
+ zone.back.y(zone.back.y() + dy);
7886
+ }
7685
7887
  }
7686
7888
  /**
7687
7889
  * Map a container-relative screen point back to world coords. Inverts the
@@ -7906,7 +8108,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7906
8108
  * must fall through and pick.
7907
8109
  */
7908
8110
  sectionBounds(id) {
7909
- const bounds2 = this.sections.filter((section) => section.id === id || section.logicalId === id).map((section) => {
8111
+ const matching = this.sections.filter((section) => section.id === id || section.logicalId === id);
8112
+ const bounds2 = matching.map((section) => {
7910
8113
  if (this.viewMode === "perspective" && section.perspectiveAffine) {
7911
8114
  return polyBounds(section.outline.map((point) => this.projectedSectionPoint(section, point)));
7912
8115
  }
@@ -7918,11 +8121,77 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7918
8121
  return polyBounds(points);
7919
8122
  });
7920
8123
  if (!bounds2.length) return null;
7921
- const left = Math.min(...bounds2.map((box) => box.x));
7922
- const top = Math.min(...bounds2.map((box) => box.y));
7923
- const right = Math.max(...bounds2.map((box) => box.x + box.width));
7924
- const bottom = Math.max(...bounds2.map((box) => box.y + box.height));
7925
- return { x: left, y: top, width: right - left, height: bottom - top };
8124
+ let left = Math.min(...bounds2.map((box) => box.x));
8125
+ let top = Math.min(...bounds2.map((box) => box.y));
8126
+ let right = Math.max(...bounds2.map((box) => box.x + box.width));
8127
+ let bottom = Math.max(...bounds2.map((box) => box.y + box.height));
8128
+ const memberIds = new Set(matching.flatMap((section) => section.memberIds));
8129
+ for (const seatId of memberIds) {
8130
+ const seat = this.seatById.get(seatId);
8131
+ if (!seat) continue;
8132
+ const point = this.renderedSeatPoint(seat);
8133
+ left = Math.min(left, point.x - this.seatR);
8134
+ top = Math.min(top, point.y - this.seatR);
8135
+ right = Math.max(right, point.x + this.seatR);
8136
+ bottom = Math.max(bottom, point.y + this.seatR);
8137
+ }
8138
+ const includeBox = (box) => {
8139
+ if (![box.x, box.y, box.width, box.height].every(Number.isFinite)) return;
8140
+ left = Math.min(left, box.x);
8141
+ top = Math.min(top, box.y);
8142
+ right = Math.max(right, box.x + box.width);
8143
+ bottom = Math.max(bottom, box.y + box.height);
8144
+ };
8145
+ const belongsToSection = (box) => {
8146
+ const centre = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
8147
+ return matching.some((section) => pointInPolygonWithHoles(centre, section.outline, section.holes));
8148
+ };
8149
+ for (const { node, backdrop } of this.freeTextById.values()) {
8150
+ const nodeBox = node.getClientRect({ relativeTo: this.bgLayer, skipShadow: true });
8151
+ if (!belongsToSection(nodeBox)) continue;
8152
+ includeBox(nodeBox);
8153
+ if (backdrop) includeBox(backdrop.getClientRect({ relativeTo: this.bgLayer, skipShadow: true }));
8154
+ }
8155
+ for (const { node } of this.iconNodeById.values()) {
8156
+ const box = node.getClientRect({ relativeTo: this.bgLayer, skipShadow: true });
8157
+ if (belongsToSection(box)) includeBox(box);
8158
+ }
8159
+ const occupiedText = new WorldBoxIndex();
8160
+ for (const { node } of this.freeTextById.values()) {
8161
+ const box = node.getClientRect({ relativeTo: this.bgLayer, skipShadow: true, skipStroke: true });
8162
+ occupiedText.insert({
8163
+ x: box.x - ROW_LABEL_CLEARANCE,
8164
+ y: box.y - ROW_LABEL_CLEARANCE,
8165
+ width: box.width + ROW_LABEL_CLEARANCE * 2,
8166
+ height: box.height + ROW_LABEL_CLEARANCE * 2
8167
+ });
8168
+ }
8169
+ for (const plan of this.rowLabelPlan) {
8170
+ const relevant = plan.focusKeys?.some((key) => key === id || matching.some((section) => key === section.id || key === section.logicalId || key === section.zone)) ?? false;
8171
+ const label = new import_Text.Text({
8172
+ text: plan.text,
8173
+ fontSize: plan.fontSize,
8174
+ fontStyle: "700",
8175
+ fontFamily: this.labelFont(),
8176
+ rotation: plan.rotation,
8177
+ listening: false
8178
+ });
8179
+ label.offsetX(label.width() / 2);
8180
+ label.offsetY(label.height() / 2);
8181
+ const resolved = this.resolveAutomaticRowLabelPosition(label, plan, occupiedText);
8182
+ const box = this.rowLabelBox(label, resolved);
8183
+ if (relevant) includeBox(box);
8184
+ occupiedText.insert(box);
8185
+ label.destroy();
8186
+ }
8187
+ const span = Math.max(right - left, bottom - top);
8188
+ const padding = Math.max(this.seatR * 2, Math.min(24, span * 0.025));
8189
+ return {
8190
+ x: left - padding,
8191
+ y: top - padding,
8192
+ width: Math.max(1, right - left + padding * 2),
8193
+ height: Math.max(1, bottom - top + padding * 2)
8194
+ };
7926
8195
  }
7927
8196
  sectionFrameScale(id) {
7928
8197
  const b = this.sectionBounds(id);
@@ -7933,7 +8202,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7933
8202
  const margin = 1.12;
7934
8203
  if (b.width <= 0 || b.height <= 0) return this.stage.scaleX();
7935
8204
  const frameScale = Math.min(w / (b.width * margin), h / (b.height * margin));
7936
- return clamp(Math.max(frameScale, SEAT_FOCUS_SCALE), min, max);
8205
+ return clamp(frameScale, min, max);
7937
8206
  }
7938
8207
  /**
7939
8208
  * Resolve a seat tap: honour the 3D deck-drill and the AXS seat-pick gate,
@@ -7946,7 +8215,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
7946
8215
  * fall through and PICK. This is the invariant: seats are never unpickable.
7947
8216
  */
7948
8217
  handleSeatTap(id) {
7949
- if (this.stacked && this.opts.onDeckTap) {
8218
+ if ((this.stacked || this.floorOverview) && this.opts.onDeckTap) {
7950
8219
  const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
7951
8220
  if (floorId) {
7952
8221
  this.opts.onDeckTap(floorId);
@@ -8021,6 +8290,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8021
8290
  });
8022
8291
  this.stage.on("wheel", (e) => {
8023
8292
  e.evt.preventDefault();
8293
+ this.releaseSectionFocusDim();
8024
8294
  const r = this.container.getBoundingClientRect();
8025
8295
  const pointer = { x: e.evt.clientX - r.left, y: e.evt.clientY - r.top };
8026
8296
  const factor = Math.exp(-e.evt.deltaY * 2e-3);
@@ -8037,8 +8307,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8037
8307
  if (near) this.handleSeatTap(near);
8038
8308
  return;
8039
8309
  }
8040
- if (this.stacked && this.opts.onDeckTap) {
8041
- const floorId = this.deckFloorAt();
8310
+ if ((this.stacked || this.floorOverview) && this.opts.onDeckTap) {
8311
+ const sectionId = this.floorOverview ? this.sectionAt(pointer) : null;
8312
+ const floorId = (sectionId ? this.objectFloor.get(sectionId) : null) ?? this.deckFloorAt();
8042
8313
  if (floorId) {
8043
8314
  this.opts.onDeckTap(floorId);
8044
8315
  return;
@@ -8436,6 +8707,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8436
8707
  ...hiddenReason ? { hiddenReason } : {}
8437
8708
  };
8438
8709
  });
8710
+ const rowLabels = this.labelGroup.find((node) => node.getAttr("rowLabel") === true).filter((node) => node instanceof import_Text.Text).map((node) => {
8711
+ const screenBounds = node.getClientRect({
8712
+ skipShadow: true,
8713
+ skipStroke: true
8714
+ });
8715
+ const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
8716
+ const ink = node.fill();
8717
+ return {
8718
+ text: node.text(),
8719
+ visible: node.isVisible() && !outside,
8720
+ renderedFontPx: rounded(node.fontSize() * effectiveScale),
8721
+ ink: typeof ink === "string" ? ink : "",
8722
+ ...!outside ? {
8723
+ screenBox: {
8724
+ x: rounded(screenBounds.x),
8725
+ y: rounded(screenBounds.y),
8726
+ width: rounded(screenBounds.width),
8727
+ height: rounded(screenBounds.height)
8728
+ }
8729
+ } : {}
8730
+ };
8731
+ });
8439
8732
  const palette = overviewPalette(this.canvasBackground);
8440
8733
  const neutralSectionFills = new Set(
8441
8734
  ["normal", "nearly-gone", "sold-out", "closed"].map((state) => overviewStateFill(palette, state).toLowerCase())
@@ -8480,7 +8773,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8480
8773
  labels,
8481
8774
  gaAreas,
8482
8775
  hierarchyLabels,
8483
- freeTextLabels
8776
+ freeTextLabels,
8777
+ rowLabels
8484
8778
  };
8485
8779
  }
8486
8780
  /** Jump the camera to a rung's zoom band (glided). */
@@ -8492,7 +8786,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8492
8786
  }
8493
8787
  const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
8494
8788
  this.seatLabelTargetScale() * 1.05,
8495
- SEAT_FOCUS_SCALE,
8789
+ SEAT_DETAIL_SCALE,
8496
8790
  CACHE_THRESHOLD * 1.3
8497
8791
  );
8498
8792
  const w = this.stage.width();
@@ -8779,14 +9073,25 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8779
9073
  for (const { objectId, node, backdrop, categoryKey, kind } of this.freeTextById.values()) {
8780
9074
  const gaDimmed = categoryKey != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaCategoryDimmed(categoryKey);
8781
9075
  const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
8782
- const visible = !gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale);
9076
+ const focusedAnnotation = kind === "free-text" && this.nodeBelongsToFocusedSection(node);
9077
+ const visible = !gaDimmed && !gaOverviewHidden && (focusedAnnotation || isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
8783
9078
  node.visible(visible);
8784
9079
  backdrop?.visible(visible);
8785
9080
  }
8786
9081
  for (const { node, fontSize } of this.iconNodeById.values()) {
8787
- node.visible(isBookableLabelLegibleAtScale(fontSize, effectiveScale));
9082
+ node.visible(this.nodeBelongsToFocusedSection(node) || isBookableLabelLegibleAtScale(fontSize, effectiveScale));
8788
9083
  }
8789
9084
  }
9085
+ /** Geometric association only: importer data remains unchanged. */
9086
+ nodeBelongsToFocusedSection(node) {
9087
+ const focus = this.focusedSectionId;
9088
+ if (!focus) return false;
9089
+ const matching = this.sections.filter((section) => section.id === focus || section.logicalId === focus || section.zone === focus);
9090
+ if (!matching.length) return false;
9091
+ const box = node.getClientRect({ relativeTo: this.bgLayer, skipShadow: true });
9092
+ const centre = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
9093
+ return matching.some((section) => pointInPolygonWithHoles(centre, section.outline, section.holes));
9094
+ }
8790
9095
  /** OV-45(a): resolve authored row-letter labels for the buyer/preview map, in
8791
9096
  * world space, matching the Designer's placement contract (start/end/both/none
8792
9097
  * preset, a free-drag `position` that wins over the preset, rotation, and
@@ -8796,18 +9101,25 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8796
9101
  this.rowLabelPlan = [];
8797
9102
  const background = this.theme.background ?? "#0e1117";
8798
9103
  const seatsByRow = /* @__PURE__ */ new Map();
9104
+ const addSeat = (key, seat) => {
9105
+ const list = seatsByRow.get(key) ?? [];
9106
+ list.push(seat);
9107
+ seatsByRow.set(key, list);
9108
+ };
8799
9109
  for (const seat of this.seats) {
8800
9110
  if (seat.kind === "booth") continue;
8801
9111
  const key = seat.logicalRowId ?? seat.rowId;
8802
- (seatsByRow.get(key) ?? seatsByRow.set(key, []).get(key)).push(seat);
9112
+ addSeat(key, seat);
9113
+ if (key !== seat.rowId) addSeat(seat.rowId, seat);
8803
9114
  }
8804
9115
  const seatOrder = (seat) => seat.logicalSeatIndex ?? Number(seat.id.split(":").pop() ?? 0);
8805
9116
  for (const obj of view.objects) {
8806
9117
  if (obj.type !== "row") continue;
8807
- if (obj.segmentedRow && obj.segmentedRow.componentIndex !== 0) continue;
8808
- const presentation = obj.segmentedRow?.labelPresentation ?? obj.labelPresentation;
9118
+ const repeatOnComponents = obj.segmentedRow?.repeatLabelOnComponents === true;
9119
+ if (obj.segmentedRow && !repeatOnComponents && obj.segmentedRow.componentIndex !== 0) continue;
9120
+ const presentation = repeatOnComponents ? obj.labelPresentation : obj.segmentedRow?.labelPresentation ?? obj.labelPresentation;
8809
9121
  if (presentation?.visible === false || presentation?.positionPreset === "none") continue;
8810
- const key = obj.segmentedRow ? obj.segmentedRow.groupId : obj.id;
9122
+ const key = obj.segmentedRow && !repeatOnComponents ? obj.segmentedRow.groupId : obj.id;
8811
9123
  const seats = (seatsByRow.get(key) ?? []).slice().sort((a, b) => seatOrder(a) - seatOrder(b));
8812
9124
  if (!seats.length) continue;
8813
9125
  const first = seats[0];
@@ -8822,6 +9134,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8822
9134
  const rotation = presentation?.rotation ?? 0;
8823
9135
  const text = obj.segmentedRow?.displayLabel ?? obj.displayLabel ?? obj.label;
8824
9136
  const opacity = this.objectFilteredOut(obj) ? 0.15 : 1;
9137
+ const containingSection = this.seatSection.get(first.id);
9138
+ const focusKeys = containingSection ? [...new Set([containingSection.id, containingSection.logicalId, containingSection.zone].filter(
9139
+ (value) => Boolean(value)
9140
+ ))] : void 0;
8825
9141
  const push = (x, y, automaticAway) => this.rowLabelPlan.push({
8826
9142
  text,
8827
9143
  x,
@@ -8830,6 +9146,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8830
9146
  fontSize,
8831
9147
  ink,
8832
9148
  opacity,
9149
+ ...focusKeys?.length ? { focusKeys } : {},
8833
9150
  ...automaticAway ? { automaticAway } : {}
8834
9151
  });
8835
9152
  if (presentation?.position) {
@@ -8849,6 +9166,18 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8849
9166
  push(last.x + away.x * gap, last.y + away.y * gap, away);
8850
9167
  }
8851
9168
  }
9169
+ const mergeRadius = (this.seatR + 12) * 2.2;
9170
+ const merged = [];
9171
+ for (const entry of this.rowLabelPlan) {
9172
+ const partner = merged.find((candidate) => candidate.text === entry.text && Math.hypot(candidate.x - entry.x, candidate.y - entry.y) <= mergeRadius);
9173
+ if (partner) {
9174
+ partner.x = (partner.x + entry.x) / 2;
9175
+ partner.y = (partner.y + entry.y) / 2;
9176
+ continue;
9177
+ }
9178
+ merged.push(entry);
9179
+ }
9180
+ this.rowLabelPlan = merged;
8852
9181
  }
8853
9182
  /** Axis-aligned world bounds of a centred, potentially rotated row label. */
8854
9183
  rowLabelBox(label, position, padding = ROW_LABEL_CLEARANCE) {
@@ -8925,12 +9254,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
8925
9254
  this.labelGroup.destroyChildren();
8926
9255
  this.labelLiftGroups.clear();
8927
9256
  this.seatLabelById.clear();
8928
- if (!show) {
8929
- this.overlayLayer.batchDraw();
8930
- return;
8931
- }
8932
9257
  let count = 0;
8933
- for (const seat of this.visibleSeatCandidates()) {
9258
+ if (show) for (const seat of this.visibleSeatCandidates()) {
8934
9259
  if (seat.kind === "booth") continue;
8935
9260
  const screen = this.worldToScreen(seat);
8936
9261
  if (screen.x < -20 || screen.x > this.stage.width() + 20 || screen.y < -20 || screen.y > this.stage.height() + 20) continue;
@@ -9021,13 +9346,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
9021
9346
  });
9022
9347
  }
9023
9348
  for (const rl of this.rowLabelPlan) {
9349
+ const focused = this.focusedSectionId != null && rl.focusKeys?.includes(this.focusedSectionId);
9350
+ const renderedFontSize = focused ? Math.max(rl.fontSize, FOCUSED_ROW_LABEL_MIN_PX / Math.max(effectiveScale, 1e-4)) : rl.fontSize;
9351
+ if (!focused && !isBookableLabelLegibleAtScale(renderedFontSize, effectiveScale)) continue;
9024
9352
  const screen = this.worldToScreen({ x: rl.x, y: rl.y });
9025
9353
  if (screen.x < -40 || screen.x > this.stage.width() + 40 || screen.y < -40 || screen.y > this.stage.height() + 40) continue;
9026
9354
  const t2 = new import_Text.Text({
9027
9355
  x: rl.x,
9028
9356
  y: rl.y,
9029
9357
  text: rl.text,
9030
- fontSize: rl.fontSize,
9358
+ fontSize: renderedFontSize,
9031
9359
  fontStyle: "700",
9032
9360
  fontFamily: this.labelFont(),
9033
9361
  fill: rl.ink,
@@ -9056,7 +9384,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
9056
9384
  const h = this.container.clientHeight || 1;
9057
9385
  if (w === this.stage.width() && h === this.stage.height()) return;
9058
9386
  this.stage.size({ width: w, height: h });
9059
- this.zoomToFit();
9387
+ this.refitCurrentView();
9060
9388
  }
9061
9389
  startFpsLoop() {
9062
9390
  const tick = (now) => {
@@ -9214,6 +9542,99 @@ function pickBestAvailable(seats, available, opts) {
9214
9542
  return { labels: ranked.slice(0, qty).map((r) => r.seat.label) };
9215
9543
  }
9216
9544
 
9545
+ // src/core/ticketRestrictions.ts
9546
+ function ticketOptionNotApplicableIssue(labels) {
9547
+ return {
9548
+ code: "ticket_option_not_applicable",
9549
+ labels,
9550
+ message: "That ticket choice is not available for the selected seat."
9551
+ };
9552
+ }
9553
+ function ticketTierAppliesToSeat(tier, seat) {
9554
+ const applicability = tier.applicability;
9555
+ if (!applicability) return true;
9556
+ if (applicability.seatLabels?.length && !applicability.seatLabels.includes(seat.label)) return false;
9557
+ if (applicability.accessibility?.length && !applicability.accessibility.some((key) => seat.accessibility?.includes(key))) return false;
9558
+ return true;
9559
+ }
9560
+ function applicableTicketTiers(doc, seat) {
9561
+ return (doc.categories.find((category) => category.key === seat.categoryKey)?.tiers ?? []).filter((tier) => ticketTierAppliesToSeat(tier, seat));
9562
+ }
9563
+ function resolveTicketTierForSeat(doc, seat, tierId) {
9564
+ const tiers = doc.categories.find((category) => category.key === seat.categoryKey)?.tiers;
9565
+ if (!tiers?.length) return { kind: "none" };
9566
+ const tier = tierId == null ? tiers.find((candidate) => ticketTierAppliesToSeat(candidate, seat)) : tiers.find((candidate) => candidate.id === tierId && ticketTierAppliesToSeat(candidate, seat));
9567
+ return tier ? { kind: "resolved", tier } : { kind: "invalid" };
9568
+ }
9569
+ function sameLogicalRow(a, b) {
9570
+ return (a.logicalRowId ?? a.rowId) === (b.logicalRowId ?? b.rowId);
9571
+ }
9572
+ function authoredIndex(seat) {
9573
+ if (Number.isInteger(seat.logicalSeatIndex)) return seat.logicalSeatIndex;
9574
+ const raw = seat.id.slice(seat.id.lastIndexOf(":") + 1);
9575
+ const index = Number(raw);
9576
+ return Number.isInteger(index) ? index : void 0;
9577
+ }
9578
+ function observablyAdjacent(a, b) {
9579
+ if (!sameLogicalRow(a, b)) return false;
9580
+ const aIndex = authoredIndex(a);
9581
+ const bIndex = authoredIndex(b);
9582
+ return aIndex != null && bIndex != null && Math.abs(aIndex - bIndex) === 1;
9583
+ }
9584
+ function companionSelectionIssue(doc, selections) {
9585
+ if (!selections.length) return null;
9586
+ const byLabel = new Map(expandChart(doc, { resolveEyeHeights: false }).map((seat) => [seat.label, seat]));
9587
+ const selected = selections.flatMap((selection) => {
9588
+ const seat = byLabel.get(selection.label);
9589
+ return seat ? [{ selection, seat }] : [];
9590
+ });
9591
+ const wheelchair = selected.map(({ seat }) => seat).filter((seat) => seat.accessibility?.includes("wheelchair"));
9592
+ const unpaired = [];
9593
+ for (const { selection, seat } of selected) {
9594
+ const resolution = resolveTicketTierForSeat(doc, seat, selection.tierId);
9595
+ const tier = resolution.kind === "resolved" ? resolution.tier : void 0;
9596
+ const isCompanion = seat.accessibility?.includes("companion") || tier?.restriction === "companion";
9597
+ if (isCompanion && !wheelchair.some((candidate) => candidate.id !== seat.id && observablyAdjacent(seat, candidate))) {
9598
+ unpaired.push(seat.label);
9599
+ }
9600
+ }
9601
+ return unpaired.length ? {
9602
+ code: "companion_pair_required",
9603
+ labels: unpaired,
9604
+ message: "Select the adjacent wheelchair place with each companion ticket."
9605
+ } : null;
9606
+ }
9607
+ function ticketSelectionIssue(doc, selections) {
9608
+ if (!selections.length) return null;
9609
+ const byLabel = new Map(expandChart(doc, { resolveEyeHeights: false }).map((seat) => [seat.label, seat]));
9610
+ const notForSaleCategories = new Set(
9611
+ doc.categories.filter((category) => category.notForSale).map((category) => category.key)
9612
+ );
9613
+ const notForSale = [];
9614
+ const inapplicable = [];
9615
+ for (const selection of selections) {
9616
+ const seat = byLabel.get(selection.label);
9617
+ if (!seat) continue;
9618
+ if (notForSaleCategories.has(seat.categoryKey)) {
9619
+ notForSale.push(seat.label);
9620
+ continue;
9621
+ }
9622
+ const resolution = resolveTicketTierForSeat(doc, seat, selection.tierId);
9623
+ if (resolution.kind === "invalid") inapplicable.push(seat.label);
9624
+ }
9625
+ if (notForSale.length) {
9626
+ return {
9627
+ code: "not_for_sale",
9628
+ labels: notForSale,
9629
+ message: "These seats are not for sale."
9630
+ };
9631
+ }
9632
+ if (inapplicable.length) {
9633
+ return ticketOptionNotApplicableIssue(inapplicable);
9634
+ }
9635
+ return companionSelectionIssue(doc, selections);
9636
+ }
9637
+
9217
9638
  // src/core/tableInventory.ts
9218
9639
  var GroupedTableProjectionError = class extends Error {
9219
9640
  constructor(objectId, message) {
@@ -9268,6 +9689,15 @@ function groupedTableInventory(doc, modelVersion) {
9268
9689
  // src/picker/PickerController.ts
9269
9690
  var DEFAULT_MAX_SELECTION = 10;
9270
9691
  var MAX_BACKOFF_MS = 15e3;
9692
+ var PickerSelectionError = class extends Error {
9693
+ constructor(issue) {
9694
+ super(issue.message);
9695
+ this.status = 422;
9696
+ this.name = "PickerSelectionError";
9697
+ this.reason = issue.code;
9698
+ this.labels = issue.labels;
9699
+ }
9700
+ };
9271
9701
  var MAP_THEME_KEYS = ["background", "rowLabelColor", "textColor", "selectionColor"];
9272
9702
  function sameMapTheme(a, b) {
9273
9703
  if (!a || !b) return !a && !b;
@@ -9397,7 +9827,9 @@ var PickerController = class {
9397
9827
  if (!r) return true;
9398
9828
  const selected = r.getSelection().map((seat) => seat.id);
9399
9829
  const floorId = this.getActiveFloorId();
9830
+ const floorOverview = r.isFloorOverview?.() ?? false;
9400
9831
  r.setChart(this.visibleDoc(), floorId ? { floorId } : void 0);
9832
+ if (floorOverview) r.setFloorOverview?.(true);
9401
9833
  if (this.opts.colorblindSafe) r.setColorblindSafe?.(true);
9402
9834
  if (this.closedSections.size) r.setClosedSections?.([...this.closedSections]);
9403
9835
  this.applySeatsMap(Object.fromEntries(this.liveStatuses), this.liveDefault);
@@ -9409,7 +9841,9 @@ var PickerController = class {
9409
9841
  const next = ids.filter((x) => typeof x === "string");
9410
9842
  if (next.length === this.hidden.size && next.every((id) => this.hidden.has(id))) return false;
9411
9843
  this.hidden = new Set(next);
9844
+ const floorOverview = this.renderer?.isFloorOverview?.() ?? false;
9412
9845
  this.renderer?.setChart(this.visibleDoc());
9846
+ if (floorOverview) this.renderer?.setFloorOverview?.(true);
9413
9847
  return true;
9414
9848
  }
9415
9849
  /** Adopt a new closed-section set; restyle (grey + non-pickable) if it differs.
@@ -9484,16 +9918,22 @@ var PickerController = class {
9484
9918
  /** Fetch chart, build label maps, mount the renderer, seed statuses, go live. */
9485
9919
  async render(host) {
9486
9920
  if (this.renderer) return null;
9921
+ const renderStartedAt = performance.now();
9922
+ const profile = {};
9923
+ const duration = (startedAt) => Math.round((performance.now() - startedAt) * 100) / 100;
9487
9924
  this.closed = false;
9488
9925
  let res;
9926
+ const chartStartedAt = performance.now();
9489
9927
  try {
9490
9928
  res = await this.api.chart(this.key);
9491
9929
  } catch (err) {
9492
9930
  this.emitError(err);
9493
9931
  return null;
9494
9932
  }
9933
+ profile.loadChart = duration(chartStartedAt);
9495
9934
  if (this.closed) return null;
9496
9935
  this._doc = res.doc;
9936
+ const normalizeStartedAt = performance.now();
9497
9937
  try {
9498
9938
  const groups = groupedTableInventory(
9499
9939
  res.doc,
@@ -9558,13 +9998,16 @@ var PickerController = class {
9558
9998
  this.labelToId.set(group.label, representative.id);
9559
9999
  this.labelToSeat.set(group.label, representative);
9560
10000
  }
10001
+ profile.normalizeBuyerModel = duration(normalizeStartedAt);
9561
10002
  const currency = res.event.currency ?? this.opts.currency;
9562
10003
  this.currency = currency ?? "USD";
10004
+ const rendererStartedAt = performance.now();
9563
10005
  const renderer = createRenderer(host, {
9564
10006
  // Group chairs are selected together for paint, but count as one logical
9565
10007
  // inventory line. The controller enforces the guest-weighted cap.
9566
10008
  maxSelection: this.rendererSelectionCap(),
9567
10009
  confirmSelection: this.opts.confirmSelection,
10010
+ portraitFitCrop: true,
9568
10011
  currency,
9569
10012
  onSelect: (seat) => {
9570
10013
  this.handleRendererSelect(seat);
@@ -9585,6 +10028,7 @@ var PickerController = class {
9585
10028
  onDeckTap: (floorId) => this.handleDeckTap(floorId),
9586
10029
  onFps: this.opts.onFps
9587
10030
  });
10031
+ profile.createRenderer = duration(rendererStartedAt);
9588
10032
  if (this.closed) {
9589
10033
  renderer.destroy();
9590
10034
  return null;
@@ -9593,6 +10037,7 @@ var PickerController = class {
9593
10037
  let seats = null;
9594
10038
  let seatsDefault = "free";
9595
10039
  let closedIds = [];
10040
+ const statusesStartedAt = performance.now();
9596
10041
  try {
9597
10042
  const objs = await this.api.objects(this.key);
9598
10043
  this.hidden = new Set(objs.hidden ?? []);
@@ -9601,18 +10046,30 @@ var PickerController = class {
9601
10046
  seatsDefault = objs.default ?? "free";
9602
10047
  } catch {
9603
10048
  }
10049
+ profile.loadStatuses = duration(statusesStartedAt);
9604
10050
  if (this.closed) {
9605
10051
  renderer.destroy();
9606
10052
  this.renderer = null;
9607
10053
  return null;
9608
10054
  }
10055
+ const sceneStartedAt = performance.now();
9609
10056
  renderer.setChart(this.visibleDoc());
10057
+ if ((res.doc.floors?.length ?? 0) > 1) renderer.setFloorOverview?.(true);
9610
10058
  if (this.opts.colorblindSafe) renderer.setColorblindSafe?.(true);
10059
+ profile.buildScene = duration(sceneStartedAt);
9611
10060
  this.closedSections = new Set(closedIds);
10061
+ const initialStateStartedAt = performance.now();
9612
10062
  if (closedIds.length) renderer.setClosedSections?.(closedIds);
9613
10063
  if (seats) this.applySeatsMap(seats, seatsDefault);
10064
+ profile.applyInitialState = duration(initialStateStartedAt);
10065
+ const firstPaintStartedAt = performance.now();
10066
+ renderer.forceDraw();
10067
+ profile.firstPaint = duration(firstPaintStartedAt);
10068
+ const realtimeStartedAt = performance.now();
9614
10069
  this.attachVisibilityListener();
9615
10070
  this.connect();
10071
+ profile.startRealtime = duration(realtimeStartedAt);
10072
+ profile.controllerTotal = duration(renderStartedAt);
9616
10073
  return {
9617
10074
  doc: res.doc,
9618
10075
  salesClosed: !!res.event.salesClosed,
@@ -9623,7 +10080,8 @@ var PickerController = class {
9623
10080
  currency,
9624
10081
  // 'test' ⇒ sandbox event: hosts render a TEST MODE ribbon (Phase 11).
9625
10082
  mode: res.event.mode ?? "live",
9626
- posterUrl: res.event.posterUrl ?? null
10083
+ posterUrl: res.event.posterUrl ?? null,
10084
+ performanceProfile: profile
9627
10085
  };
9628
10086
  }
9629
10087
  // ---- selection ------------------------------------------------------------
@@ -9679,11 +10137,23 @@ var PickerController = class {
9679
10137
  select(ids) {
9680
10138
  const before = new Set(this.getSelection().map((seat) => seat.label));
9681
10139
  const expanded = /* @__PURE__ */ new Set();
10140
+ const rejected = [];
9682
10141
  for (const id of ids) {
9683
10142
  const table = this.groupedTableBySeatId.get(id) ?? this.groupedTablesByLabel.get(id);
9684
- if (table) table.chairs.forEach((chair) => expanded.add(chair.id));
9685
- else expanded.add(id);
10143
+ if (table) {
10144
+ const resolution = this.tableTierResolution(table);
10145
+ if (resolution.kind === "invalid") rejected.push(table.label);
10146
+ else table.chairs.forEach((chair) => expanded.add(chair.id));
10147
+ } else {
10148
+ const seat = this.seatById.get(id);
10149
+ if (seat && this.seatTierResolution(seat).kind === "invalid") rejected.push(seat.label);
10150
+ else expanded.add(id);
10151
+ }
9686
10152
  }
10153
+ if (rejected.length) {
10154
+ this.emitError(new PickerSelectionError(ticketOptionNotApplicableIssue(rejected)));
10155
+ }
10156
+ if (!expanded.size) return [];
9687
10157
  this.renderer?.select?.([...expanded]);
9688
10158
  if (this.selectionGuestCount() > this.maxSelection) {
9689
10159
  this.renderer?.deselect([...expanded]);
@@ -9751,6 +10221,13 @@ var PickerController = class {
9751
10221
  }
9752
10222
  handleRendererSelect(seat) {
9753
10223
  const table = this.groupedTableBySeatId.get(seat.id);
10224
+ const resolution = table ? this.tableTierResolution(table) : this.seatTierResolution(seat);
10225
+ if (resolution.kind === "invalid") {
10226
+ this.renderer?.deselect(table ? table.chairs.map((chair) => chair.id) : [seat.id]);
10227
+ this.emitError(new PickerSelectionError(ticketOptionNotApplicableIssue([table?.label ?? seat.label])));
10228
+ this.emitSelectionChange();
10229
+ return;
10230
+ }
9754
10231
  if (table) {
9755
10232
  if (this.selectionGuestCount() > this.maxSelection) {
9756
10233
  this.renderer?.deselect(table.chairs.map((chair) => chair.id));
@@ -9832,6 +10309,8 @@ var PickerController = class {
9832
10309
  const existingGA = (this.hold_?.items ?? []).filter((item) => item.objectType === "ga");
9833
10310
  const combinedLabels = [.../* @__PURE__ */ new Set([...existingGA.map((item) => item.label), ...labels])];
9834
10311
  if (!combinedLabels.length) return null;
10312
+ const restriction = this.ticketIssue(combinedLabels);
10313
+ if (restriction) throw new PickerSelectionError(restriction);
9835
10314
  if (this.hold_ && this.holdCovers(combinedLabels)) return this.hold_;
9836
10315
  try {
9837
10316
  const selections = combinedLabels.map((label) => {
@@ -10003,6 +10482,7 @@ var PickerController = class {
10003
10482
  const available = /* @__PURE__ */ new Set();
10004
10483
  for (const seat of seats) {
10005
10484
  if (held.has(seat.label)) continue;
10485
+ if (this.seatTierResolution(seat).kind === "invalid") continue;
10006
10486
  const id = this.labelToId.get(seat.label);
10007
10487
  const status = id ? this.getStatus(id) : void 0;
10008
10488
  if ((status ?? "free") === "free") available.add(seat.label);
@@ -10084,6 +10564,8 @@ var PickerController = class {
10084
10564
  if (!this.api.book) throw new Error("picker: transport has no book() \u2014 hold-only mode");
10085
10565
  const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
10086
10566
  if (!labels.length) return null;
10567
+ const restriction = this.ticketIssue(labels);
10568
+ if (restriction) throw new PickerSelectionError(restriction);
10087
10569
  let holdId;
10088
10570
  try {
10089
10571
  if (this.hold_ && this.holdCovers(labels)) {
@@ -10210,6 +10692,20 @@ var PickerController = class {
10210
10692
  getStatus(id) {
10211
10693
  return this.renderer?.getStatus(id);
10212
10694
  }
10695
+ /** Seat ids whose category the author marked not-for-sale. */
10696
+ notForSaleCategorySeatIds() {
10697
+ const doc = this.doc;
10698
+ if (!doc) return [];
10699
+ const flagged = new Set(
10700
+ doc.categories.filter((category) => category.notForSale).map((category) => category.key)
10701
+ );
10702
+ if (!flagged.size) return [];
10703
+ const ids = [];
10704
+ for (const [id, seat] of this.seatById) {
10705
+ if (flagged.has(seat.categoryKey)) ids.push(id);
10706
+ }
10707
+ return ids;
10708
+ }
10213
10709
  flashSeat(id, color) {
10214
10710
  this.renderer?.flashSeat(id, color);
10215
10711
  }
@@ -10222,6 +10718,11 @@ var PickerController = class {
10222
10718
  zoomToFit() {
10223
10719
  this.renderer?.zoomToFit();
10224
10720
  }
10721
+ /** Preserve section focus while the usable map viewport changes. */
10722
+ refitCurrentView() {
10723
+ if (this.renderer?.refitCurrentView) this.renderer.refitCurrentView();
10724
+ else this.renderer?.zoomToFit();
10725
+ }
10225
10726
  worldToScreen(p) {
10226
10727
  return this.renderer?.worldToScreen(p) ?? { x: 0, y: 0 };
10227
10728
  }
@@ -10251,7 +10752,8 @@ var PickerController = class {
10251
10752
  /** Switch the shown floor (2D), then re-apply live seat statuses onto it. */
10252
10753
  setFloor(id) {
10253
10754
  const r = this.renderer;
10254
- if (!r?.setActiveFloor || id === r.getActiveFloorId?.()) return;
10755
+ if (!r?.setActiveFloor || id === r.getActiveFloorId?.() && !r.isFloorOverview?.()) return;
10756
+ r.setFloorOverview?.(false);
10255
10757
  r.setStacked?.(false);
10256
10758
  r.setActiveFloor(id);
10257
10759
  void this.resnapshot();
@@ -10267,12 +10769,24 @@ var PickerController = class {
10267
10769
  isMultiFloor() {
10268
10770
  return (this._doc?.floors?.length ?? 0) > 1;
10269
10771
  }
10772
+ /** Enter/leave the source-coordinate all-floor buyer overview. */
10773
+ setFloorOverview(on) {
10774
+ const r = this.renderer;
10775
+ if (!r?.setFloorOverview || on === r.isFloorOverview?.()) return;
10776
+ r.setFloorOverview(on);
10777
+ void this.resnapshot();
10778
+ this.emitSelectionChange();
10779
+ }
10780
+ isFloorOverview() {
10781
+ return this.renderer?.isFloorOverview?.() ?? false;
10782
+ }
10270
10783
  /** Tap-a-deck-to-enter: leave the 3D stack, drop to flat 2D on `floorId`, and
10271
10784
  * tell the host so it can sync its 2D/3D toggle + floor-switcher state. */
10272
10785
  handleDeckTap(floorId) {
10273
10786
  const r = this.renderer;
10274
10787
  if (!r) return;
10275
10788
  r.setViewMode?.("flat");
10789
+ r.setFloorOverview?.(false);
10276
10790
  r.setStacked?.(false);
10277
10791
  r.setActiveFloor?.(floorId);
10278
10792
  void this.resnapshot();
@@ -10341,10 +10855,30 @@ var PickerController = class {
10341
10855
  if (!r?.getVisibleWorldRect || !r.getWorldBounds) return null;
10342
10856
  return { visible: r.getVisibleWorldRect(), bounds: r.getWorldBounds() };
10343
10857
  }
10858
+ getMinimapSnapshot() {
10859
+ return this.renderer?.getMinimapSnapshot?.() ?? { sections: [], seats: [] };
10860
+ }
10861
+ panToWorld(point) {
10862
+ this.renderer?.panToWorld?.(point);
10863
+ }
10864
+ /** Leave section-focus dimming without changing the current zoom. Used when
10865
+ * the minimap viewport itself is dragged to a different part of the venue. */
10866
+ clearSectionFocus() {
10867
+ this.renderer?.clearSectionFocus?.();
10868
+ this.opts.onSectionFocus?.(null);
10869
+ }
10870
+ /** Read-only browser-render evidence for parity/performance audit tooling. */
10871
+ getRenderedQualityEvidence() {
10872
+ return this.renderer?.getRenderedQualityEvidence() ?? null;
10873
+ }
10344
10874
  /** Glide in on a section and surface its summary (same path as a section tap). */
10345
10875
  focusSection(id) {
10346
10876
  this.handleSectionTap(id);
10347
10877
  }
10878
+ /** Live seat membership count used by buyer section-navigation controls. */
10879
+ getSectionSeatCount(id) {
10880
+ return this.renderer?.sectionMembers?.(id)?.length ?? 0;
10881
+ }
10348
10882
  /** Zoom back out to the whole chart and clear the section-summary card + focus. */
10349
10883
  overview() {
10350
10884
  this.renderer?.clearSectionFocus?.();
@@ -10397,15 +10931,21 @@ var PickerController = class {
10397
10931
  }
10398
10932
  const categories = [...byCat.entries()].map(([key, count]) => {
10399
10933
  const cat = doc.categories.find((c) => c.key === key);
10934
+ const prices = cat?.tiers?.length ? cat.tiers.map((tier) => tier.price) : [cat?.price ?? 0];
10935
+ const priceMin = Math.min(...prices);
10936
+ const priceMax = Math.max(...prices);
10400
10937
  return {
10401
10938
  key,
10402
10939
  count,
10403
10940
  label: cat?.label ?? key,
10404
10941
  color: cat?.color ?? "#6e7bff",
10405
- price: cat?.price ?? 0
10942
+ price: priceMin,
10943
+ priceMin,
10944
+ priceMax
10406
10945
  };
10407
10946
  }).sort((a, b) => a.price - b.price);
10408
- const prices = categories.map((c) => c.price);
10947
+ const minimums = categories.map((c) => c.priceMin);
10948
+ const maximums = categories.map((c) => c.priceMax);
10409
10949
  const zone = sec.zone ? doc.zones?.find((z) => z.id === sec.zone) : void 0;
10410
10950
  const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
10411
10951
  return {
@@ -10415,8 +10955,8 @@ var PickerController = class {
10415
10955
  ...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
10416
10956
  color,
10417
10957
  seatsLeft,
10418
- priceMin: prices.length ? prices[0] : 0,
10419
- priceMax: prices.length ? prices[prices.length - 1] : 0,
10958
+ priceMin: minimums.length ? Math.min(...minimums) : 0,
10959
+ priceMax: maximums.length ? Math.max(...maximums) : 0,
10420
10960
  categories
10421
10961
  };
10422
10962
  }
@@ -10457,11 +10997,12 @@ var PickerController = class {
10457
10997
  const accessibility = s.accessibility?.length ? { accessibility: s.accessibility } : void 0;
10458
10998
  const wheelchair = s.wheelchairSpaceType ? { wheelchairSpaceType: s.wheelchairSpaceType } : void 0;
10459
10999
  const context = this.seatContext.get(s.id);
10460
- const tiers = this.tiersFor(s.categoryKey);
10461
- if (!tiers) {
10462
- return { id: s.id, label: s.label, ...display, ...context, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial, ...accessibility, ...wheelchair };
11000
+ const tiers = this.tiersForSeat(s);
11001
+ const resolution = this.seatTierResolution(s);
11002
+ if (resolution.kind !== "resolved" || !tiers) {
11003
+ return { id: s.id, label: s.label, ...display, ...context, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), currency: this.currency, ...commercial, ...accessibility, ...wheelchair };
10463
11004
  }
10464
- const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
11005
+ const chosen = resolution.tier;
10465
11006
  return {
10466
11007
  id: s.id,
10467
11008
  label: s.label,
@@ -10469,6 +11010,7 @@ var PickerController = class {
10469
11010
  ...context,
10470
11011
  categoryKey: s.categoryKey,
10471
11012
  price: chosen.price,
11013
+ currency: this.currency,
10472
11014
  tiers,
10473
11015
  tierId: chosen.id,
10474
11016
  ...commercial,
@@ -10478,8 +11020,9 @@ var PickerController = class {
10478
11020
  }
10479
11021
  toTableSeat(table) {
10480
11022
  const representative = table.chairs[0];
10481
- const tiers = this.tiersFor(table.categoryKey);
10482
- const chosen = tiers?.find((tier) => tier.id === this.seatTiers.get(representative?.id ?? "")) ?? tiers?.[0];
11023
+ const tiers = this.tiersFor(table.categoryKey)?.filter((tier) => resolveTicketTierForSeat(this._doc, this.tableTierSeat(table), tier.id).kind === "resolved");
11024
+ const resolution = this.tableTierResolution(table);
11025
+ const chosen = resolution.kind === "resolved" ? resolution.tier : void 0;
10483
11026
  return {
10484
11027
  id: representative?.id ?? table.objectId,
10485
11028
  label: table.label,
@@ -10490,6 +11033,7 @@ var PickerController = class {
10490
11033
  rowLabel: table.displayLabel ?? table.label,
10491
11034
  categoryKey: table.categoryKey,
10492
11035
  price: chosen?.price ?? this.priceFor(table.categoryKey),
11036
+ currency: this.currency,
10493
11037
  ...tiers ? { tiers, tierId: chosen?.id } : {},
10494
11038
  objectType: "table",
10495
11039
  bookingMode: table.mode,
@@ -10519,6 +11063,27 @@ var PickerController = class {
10519
11063
  const t2 = this._doc?.categories.find((c) => c.key === categoryKey)?.tiers;
10520
11064
  return t2 && t2.length ? t2 : void 0;
10521
11065
  }
11066
+ tiersForSeat(seat) {
11067
+ if (!this._doc) return void 0;
11068
+ const tiers = applicableTicketTiers(this._doc, seat);
11069
+ return tiers.length ? tiers : void 0;
11070
+ }
11071
+ seatTierResolution(seat) {
11072
+ if (!this._doc) return { kind: "none" };
11073
+ return resolveTicketTierForSeat(this._doc, seat, this.seatTiers.get(seat.id));
11074
+ }
11075
+ tableTierSeat(table) {
11076
+ return {
11077
+ label: table.label,
11078
+ categoryKey: table.categoryKey,
11079
+ accessibility: table.chairs.flatMap((chair) => chair.accessibility ?? [])
11080
+ };
11081
+ }
11082
+ tableTierResolution(table) {
11083
+ if (!this._doc) return { kind: "none" };
11084
+ const representativeId = table.chairs[0]?.id ?? "";
11085
+ return resolveTicketTierForSeat(this._doc, this.tableTierSeat(table), this.seatTiers.get(representativeId));
11086
+ }
10522
11087
  /**
10523
11088
  * Choose a ticket tier for a selected seat (e.g. Adult → Child). Re-emits the
10524
11089
  * selection so the host's cart + the eventual hold carry the new tier + price.
@@ -10528,7 +11093,7 @@ var PickerController = class {
10528
11093
  setSeatTier(seatId, tierId) {
10529
11094
  const seat = this.seatById.get(seatId);
10530
11095
  if (!seat) return;
10531
- const tiers = this.tiersFor(seat.categoryKey);
11096
+ const tiers = this.tiersForSeat(seat);
10532
11097
  if (!tiers) return;
10533
11098
  if (tierId == null) this.seatTiers.delete(seatId);
10534
11099
  else if (tiers.some((t2) => t2.id === tierId)) this.seatTiers.set(seatId, tierId);
@@ -10549,6 +11114,16 @@ var PickerController = class {
10549
11114
  this.opts.onSelectionChange?.(this.getSelection());
10550
11115
  this.emitOrphanHint();
10551
11116
  }
11117
+ /** Check explicit tier applicability and companion relationships for the
11118
+ * current (or supplied) seats. Ticket display names are never semantic input. */
11119
+ ticketIssue(labels) {
11120
+ if (!this._doc) return null;
11121
+ const seats = labels ? this.seatsForLabels(labels) : this.getSelection();
11122
+ return ticketSelectionIssue(this._doc, seats.map((seat) => ({
11123
+ label: seat.label,
11124
+ ...seat.tierId ? { tierId: seat.tierId } : {}
11125
+ })));
11126
+ }
10552
11127
  /**
10553
11128
  * Orphan-seat advice on manual selection: when the buyer's current picks
10554
11129
  * strand one (or more) single free seats between unavailable same-row
@@ -10559,6 +11134,12 @@ var PickerController = class {
10559
11134
  if (!this.opts.onHint) return;
10560
11135
  const r = this.renderer;
10561
11136
  if (!r) return;
11137
+ const restriction = this.ticketIssue();
11138
+ if (restriction) {
11139
+ this.hintShown = true;
11140
+ this.opts.onHint(restriction.code === "companion_pair_required" ? t("picker.companionPairRequired") : "That ticket choice is not available for the selected seat.");
11141
+ return;
11142
+ }
10562
11143
  const selected = new Set(r.getSelection().map((s) => s.id));
10563
11144
  const stranded = selected.size ? strandedSingles(this.seatById.values(), (id) => r.getStatus(id), selected) : [];
10564
11145
  if (stranded.length > 0) {
@@ -10680,6 +11261,8 @@ var PickerController = class {
10680
11261
  ["free", "held", "booked", "not_for_sale"].forEach((st) => {
10681
11262
  if (st !== defaultSeatStatus && byStatus[st].length) r.setStatus(byStatus[st], st);
10682
11263
  });
11264
+ const notForSaleCategoryIds = this.notForSaleCategorySeatIds();
11265
+ if (notForSaleCategoryIds.length) r.setStatus(notForSaleCategoryIds, "not_for_sale");
10683
11266
  this.clearBookedHoldIfSettled();
10684
11267
  this.opts.onStatusChange?.();
10685
11268
  }
@@ -12065,6 +12648,7 @@ async function loadLocale(code) {
12065
12648
  MAX_EVENT_INVENTORY,
12066
12649
  MAX_GA_CAPACITY,
12067
12650
  PickerController,
12651
+ PickerSelectionError,
12068
12652
  RENDERED_QUALITY_REPORT_VERSION,
12069
12653
  SEAT_COMMERCIAL_MARKS,
12070
12654
  SUPPORTED_LOCALES,
@@ -12088,6 +12672,7 @@ async function loadLocale(code) {
12088
12672
  expandTable,
12089
12673
  expandTableSlots,
12090
12674
  floorObjects,
12675
+ floorOverviewTranslations,
12091
12676
  floorsOf,
12092
12677
  formatDate,
12093
12678
  formatMinor,
@@ -12106,6 +12691,7 @@ async function loadLocale(code) {
12106
12691
  isGaUnitLabel,
12107
12692
  isSectionHidden,
12108
12693
  layerOf,
12694
+ layoutFloorOverview,
12109
12695
  loadLocale,
12110
12696
  objectCenter,
12111
12697
  owningSectionForObject,
@@ -12125,6 +12711,7 @@ async function loadLocale(code) {
12125
12711
  tCount,
12126
12712
  tableInventoryCount,
12127
12713
  tableSeatCountsBySide,
12714
+ translateObject,
12128
12715
  validGAInventorySegments
12129
12716
  });
12130
12717
  //# sourceMappingURL=index.cjs.map