@seatlayer/core 0.50.2 → 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 (40) 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-YGODYJ3E.js +292 -0
  6. package/dist/de-YGODYJ3E.js.map +1 -0
  7. package/dist/es-VMBTGVFQ.js +292 -0
  8. package/dist/es-VMBTGVFQ.js.map +1 -0
  9. package/dist/fr-SLBE2ULB.js +292 -0
  10. package/dist/fr-SLBE2ULB.js.map +1 -0
  11. package/dist/index.cjs +1606 -64
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +138 -17
  14. package/dist/index.d.ts +138 -17
  15. package/dist/index.js +829 -65
  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-ST4H423D.js +0 -56
  36. package/dist/de-ST4H423D.js.map +0 -1
  37. package/dist/es-WEC5NHRT.js +0 -56
  38. package/dist/es-WEC5NHRT.js.map +0 -1
  39. package/dist/fr-SGBFNAQV.js +0 -56
  40. package/dist/fr-SGBFNAQV.js.map +0 -1
package/dist/index.js CHANGED
@@ -22,8 +22,10 @@ import {
22
22
  expandTable,
23
23
  expandTableSlots,
24
24
  floorObjects,
25
+ floorOverviewTranslations,
25
26
  floorsOf,
26
27
  layerOf,
28
+ layoutFloorOverview,
27
29
  objectCenter,
28
30
  owningSectionForObject,
29
31
  pointInPolygon,
@@ -37,8 +39,9 @@ import {
37
39
  sectionGeometry,
38
40
  stackFloors,
39
41
  tableInventoryCount,
40
- tableSeatCountsBySide
41
- } from "./chunk-GD267BFB.js";
42
+ tableSeatCountsBySide,
43
+ translateObject
44
+ } from "./chunk-UID7KN44.js";
42
45
 
43
46
  // src/core/ga.ts
44
47
  var PREFIX = "__sl_ga__";
@@ -1286,6 +1289,8 @@ var en = {
1286
1289
  "picker.poweredBy": "Powered by SeatLayer",
1287
1290
  "picker.testMode": "TEST MODE",
1288
1291
  "picker.orphanHint": "This leaves a single seat stranded \u2014 consider shifting one seat over.",
1292
+ "picker.companionRequiresWheelchair": "Requires the adjacent wheelchair place",
1293
+ "picker.companionPairRequired": "Select the adjacent wheelchair place with each companion ticket.",
1289
1294
  // renderer (drawn on the Konva map — shared by the embed SDK)
1290
1295
  "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
1291
1296
  "map.seatsLeft": "{count} LEFT",
@@ -1324,7 +1329,241 @@ var en = {
1324
1329
  "picker.premiumFallbackNote": "No premium block of {count} \u2014 showing best overall",
1325
1330
  "picker.loading3d": "Building the 3D venue\u2026",
1326
1331
  "picker.unavailable3d": "3D could not start. The seat map is still available.",
1327
- "picker.jumpToSection": "Jump to section"
1332
+ "picker.jumpToSection": "Jump to section",
1333
+ "picker.salesClosedPill": "Sales are closed",
1334
+ "picker.salesClosedCopy": "Ticket sales for this event have ended.",
1335
+ "picker.salesClosedCta": "Sales closed",
1336
+ "picker.salesClosedToast": "Sales are closed for this event.",
1337
+ "picker.holdReassurance": "Yours for {time} \u2014 you won\u2019t be charged yet.",
1338
+ "picker.securingSeats": "Securing your seats\u2026",
1339
+ "picker.openingCheckout": "Opening secure checkout\u2026",
1340
+ "picker.peekSecured": "\u2713 {count} secured \xB7 {total} \u2014 you won\u2019t be charged yet",
1341
+ "picker.priceHint": "Colours on the map are ticket types \u2014 tap one below to show just those seats.",
1342
+ "picker.findTogetherInstead": "Find seats together instead",
1343
+ "picker.keepMyPicks": "Keep my picks",
1344
+ // picker widget — buyer-facing SeatPicker.ts strings (localization pass)
1345
+ "picker.accessExpiredBody": "Sign in again, or reload the page, to keep browsing these seats. Anything you are already holding stays yours.",
1346
+ "picker.accessExpiredTitle": "Your access session has ended",
1347
+ "picker.accessInvalidBody": "You can still book anything shown as available. Contact whoever sent you here for access to the rest.",
1348
+ "picker.accessInvalidTitle": "We couldn\u2019t verify your access",
1349
+ "picker.accessPausedBody": "The organizer has paused this selection. Try again in a few minutes.",
1350
+ "picker.accessPausedTitle": "These seats are on hold right now",
1351
+ "picker.accessRetry": "Try again",
1352
+ "picker.accessRevokedBody": "Ask whoever sent you here for a new link to keep booking these seats.",
1353
+ "picker.accessRevokedTitle": "This access link is no longer active",
1354
+ "picker.accessiblePhysicalSeat": "Accessible physical seat",
1355
+ "picker.addTime": "Add time",
1356
+ "picker.addingEllipsis": "Adding\u2026",
1357
+ "picker.allLevels": "All levels",
1358
+ "picker.allPlacesBookedTogether": "All {count} places are booked together as one exclusive table.",
1359
+ "picker.allPrices": "All prices",
1360
+ "picker.allSeats": "All seats",
1361
+ "picker.allSetTitle": "You\u2019re all set",
1362
+ "picker.anyTicketType": "Any ticket type",
1363
+ "picker.anyVenueZone": "Any venue zone",
1364
+ "picker.areas": "Areas",
1365
+ "picker.available": "Available",
1366
+ "picker.backToMap": "Back to map",
1367
+ "picker.backToVenue": "Back to venue",
1368
+ "picker.bestSeatsStar": "\u2726 Best seats",
1369
+ "picker.booth": "Booth",
1370
+ "picker.capacityGuests": "{count} guests",
1371
+ "picker.change": "Change",
1372
+ "picker.chartDerivedModel": "Chart-derived model",
1373
+ "picker.chartDerivedSeatEye": "Live 3D \xB7 chart-derived seat-eye \xB7 not surveyed",
1374
+ "picker.checkConnection": "Check your connection and try again.",
1375
+ "picker.checkoutCouldNotBeOpened": "Checkout could not be opened. Your seats are still held \u2014 please try again.",
1376
+ "picker.chooseAnotherToCompare": "Choose another seat to compare.",
1377
+ "picker.chooseGuestsCopy": "Choose how many guests will sit together. This table is held exclusively for your party.",
1378
+ "picker.chooseMinMaxGuests": "Choose {min}\u2013{max} guests",
1379
+ "picker.clearSavedSeatComparison": "Clear saved seat comparison",
1380
+ "picker.close": "Close",
1381
+ "picker.closeSeatStatus": "Close seat status",
1382
+ "picker.closestGroupChosenInstantly": "Closest available group, chosen instantly.",
1383
+ "picker.collapseTicketPanel": "Collapse ticket panel",
1384
+ "picker.compareCount": "Compare {count}",
1385
+ "picker.compareWithSaved": "Compare with saved",
1386
+ "picker.confirmOrCancelSeat": "Confirm or cancel this seat",
1387
+ "picker.confirmSeatLabel": "Confirm seat {label}",
1388
+ "picker.confirmYourTable": "Confirm your table",
1389
+ "picker.confirmedAndOnWay": "confirmed. A confirmation is on its way.",
1390
+ "picker.continue": "Continue",
1391
+ "picker.continueToCheckout": "Continue to checkout",
1392
+ "picker.couldNotAddMoreTime": "Couldn\u2019t add more time \u2014 please head to checkout now.",
1393
+ "picker.couldNotFindSeatsTogether": "We couldn\u2019t find {count} seats together. Try fewer seats or another ticket type.",
1394
+ "picker.couldNotReleaseTickets": "Couldn\u2019t release your tickets. Your hold is unchanged.",
1395
+ "picker.couldNotRemoveLabel": "Couldn\u2019t remove {label}. Your hold is unchanged.",
1396
+ "picker.currentOffer": "Current offer",
1397
+ "picker.currentTicketOffer": "Current ticket offer",
1398
+ "picker.dragToLookAround": "Drag to look around \xB7 scroll to zoom",
1399
+ "picker.emptyWheelchairSpace": "Empty wheelchair space",
1400
+ "picker.exitFullScreen": "Exit full screen",
1401
+ "picker.fewer": "Fewer",
1402
+ "picker.fewerGuests": "Fewer guests",
1403
+ "picker.fewerSeats": "Fewer seats",
1404
+ "picker.filterAndFocusByPrice": "Filter and focus seats by price",
1405
+ "picker.filters": "Filters",
1406
+ "picker.findBestSeatsCount.one": "Find {count} best seat",
1407
+ "picker.findBestSeatsCount.other": "Find {count} best seats",
1408
+ "picker.findBestSeatsTogether": "Find the best seats together",
1409
+ "picker.findNewSeats": "Find new seats",
1410
+ "picker.findingBestSeats": "Finding the best seats\u2026",
1411
+ "picker.findingEllipsis": "Finding\u2026",
1412
+ "picker.fitToScreen": "Fit to screen",
1413
+ "picker.flat2dMap": "Flat 2D map",
1414
+ "picker.flexiblePartyTypeWord": "Flexible party \xB7 {typeWord}",
1415
+ "picker.fullScreen": "Full screen",
1416
+ "picker.generalAdmission": "General admission",
1417
+ "picker.guestCountCouldNotBeSecured": "That guest count could not be secured. Your current table hold is unchanged.",
1418
+ "picker.guestCountNoLongerAvailable": "That guest count is no longer available. Your current hold is unchanged.",
1419
+ "picker.guests": "Guests",
1420
+ "picker.guestsCount": "{count} guests",
1421
+ "picker.guestsCountEdit": "{count} guests \xB7 Edit",
1422
+ "picker.held": "Held",
1423
+ "picker.heldForYou": "Held for you",
1424
+ "picker.heldSeatExplanation": "Another buyer is holding this seat. It may become available again.",
1425
+ "picker.heldTicketsReleased": "Held tickets released. Choose your new seats.",
1426
+ "picker.heldTicketsRestored": "Your held tickets have been restored.",
1427
+ "picker.hidePanel": "Hide panel",
1428
+ "picker.hideTicketPanel": "Hide the ticket panel",
1429
+ "picker.holdSeatsAndCheckout": "Hold seats & checkout",
1430
+ "picker.howOfferWorks": "How the {name} offer works",
1431
+ "picker.interactive3dVenueView": "Interactive 3D venue view",
1432
+ "picker.keepMine": "Keep mine",
1433
+ "picker.labelNoLongerAvailable": "{label} is no longer available.",
1434
+ "picker.labelRemoved": "{label} removed.",
1435
+ "picker.labelRemovedFromHold": "{label} removed from your hold.",
1436
+ "picker.labelRestored": "{label} restored.",
1437
+ "picker.labelsNoLongerAvailable.one": "{labels} is no longer available. Choose another seat.",
1438
+ "picker.labelsNoLongerAvailable.other": "{labels} are no longer available. Choose another group.",
1439
+ "picker.leftCount.one": "{count} left",
1440
+ "picker.leftCount.other": "{count} left",
1441
+ "picker.levelNumber": "Level {number}",
1442
+ "picker.levels": "Levels",
1443
+ "picker.liveAvailability": "Live availability \u2014 seats update in real time",
1444
+ "picker.loadingSeatMap": "Loading seat map\u2026",
1445
+ "picker.lookAroundLive3d": "Look around in live 3D",
1446
+ "picker.manualTicketsRemovedNote": "Your manually selected tickets will be removed only after a new group is secured.",
1447
+ "picker.map": "Map",
1448
+ "picker.mapDidNotLoad": "The seat map didn\u2019t load",
1449
+ "picker.maxTicketsForOrder": "You can select up to {count} tickets for this order.",
1450
+ "picker.minToMaxGuests": "{min} to {max} guests",
1451
+ "picker.more": "More",
1452
+ "picker.moreGuests": "More guests",
1453
+ "picker.moreSeats": "More seats",
1454
+ "picker.moreSelectedCount": "{count} more selected",
1455
+ "picker.moreTimeAdded": "More time added \u2014 your seats are still held.",
1456
+ "picker.noAccessibilityMetadata": "No accessibility metadata supplied",
1457
+ "picker.noAuthoredRestriction": "No organizer-authored restriction",
1458
+ "picker.noSeatsSelected": "No seats selected",
1459
+ "picker.notChargedYet": "You won\u2019t be charged yet.",
1460
+ "picker.notForSale": "Not for sale",
1461
+ "picker.notForSaleExplanation": "This seat is not included in the current sale.",
1462
+ "picker.numberOfGuests": "Number of guests",
1463
+ "picker.offerDetailActive": "This price applies automatically to eligible seats. Tickets in active carts temporarily reduce the available quantity; released or expired holds return it. When the offer ends, the next matching offer or normal ticket price takes over.",
1464
+ "picker.offerDetailUpcoming": "Tickets are available at their normal price now. This scheduled offer will apply automatically to eligible seats when it starts.",
1465
+ "picker.offerRemainingAvailable": "{count} available",
1466
+ "picker.offerStarts": "starts {time}",
1467
+ "picker.offerUntil": "until {time}",
1468
+ "picker.oneSeatSaved": "1 seat saved",
1469
+ "picker.oneSeatSavedChooseAnother": "One seat saved; choose another to compare",
1470
+ "picker.openAuthored360": "Open venue 360\xB0",
1471
+ "picker.openComparison": "Open comparison",
1472
+ "picker.openComparisonOfSeats": "Open comparison of {count} seats",
1473
+ "picker.openTicketPanel": "Open ticket panel",
1474
+ "picker.peekFromPrice": "From {price}",
1475
+ "picker.peekTicketsTotal.one": "{count} ticket \xB7 {total}",
1476
+ "picker.peekTicketsTotal.other": "{count} tickets \xB7 {total}",
1477
+ "picker.perGuestPrice": "{price} per guest",
1478
+ "picker.pickYourSeats": "Pick your seats",
1479
+ "picker.preferredTicketType": "Preferred ticket type",
1480
+ "picker.preferredVenueZone": "Preferred venue zone",
1481
+ "picker.priceNotSupplied": "Price not supplied",
1482
+ "picker.releaseHeldTickets": "Release held tickets and choose different seats",
1483
+ "picker.releasingEllipsis": "Releasing\u2026",
1484
+ "picker.removeHeldTicketLabel": "Remove held ticket {label}",
1485
+ "picker.removeSeatLabel": "Remove {label}",
1486
+ "picker.removeTicketsUntilOrFewer": "Remove tickets until your order has {count} or fewer.",
1487
+ "picker.replaceCurrentChoices": "Replace your current choices?",
1488
+ "picker.review": "Review",
1489
+ "picker.row": "Row",
1490
+ "picker.rowLabel": "Row {label}",
1491
+ "picker.saveToCompare": "Save to compare",
1492
+ "picker.savedForComparison": "Saved for comparison",
1493
+ "picker.savedSeatComparison": "Saved seat comparison",
1494
+ "picker.scheduledOffer": "Scheduled offer",
1495
+ "picker.seat": "Seat",
1496
+ "picker.seatCount.one": "{count} seat",
1497
+ "picker.seatCount.other": "{count} seats",
1498
+ "picker.seatJustTakenByAnother": "Seat {label} was just taken by another buyer.",
1499
+ "picker.seatLabel": "Seat {label}",
1500
+ "picker.seatNoLongerAvailable": "That seat is no longer available.",
1501
+ "picker.seatNoLongerYours": "Some seats are no longer available to you. They have been removed from your order.",
1502
+ "picker.seatNumberLower": "seat {label}",
1503
+ "picker.seatSalesClosedForEvent": "Seat sales have closed for this event.",
1504
+ "picker.seatSelection": "Seat selection",
1505
+ "picker.seatStatusLegend": "Seat status legend",
1506
+ "picker.seatTaken": "Someone else took a seat you had picked. It has been removed from your order.",
1507
+ "picker.seatsHeldForNeedMoreTime": "Your seats are held for <b>{time}</b>. Need more time?",
1508
+ "picker.seatsJustTaken": "One or more seats were just taken. Please pick again.",
1509
+ "picker.seatsJustTakenInCategory.one": "{count} seat just taken in {label} \xB7 {left} left",
1510
+ "picker.seatsJustTakenInCategory.other": "{count} seats just taken in {label} \xB7 {left} left",
1511
+ "picker.seatsNoLongerAvailableTryAnother": "Those seats are no longer available. Try another quantity or ticket type.",
1512
+ "picker.seatsSecured": "Seats secured",
1513
+ "picker.seatsSelectedCount.one": "{count} selected",
1514
+ "picker.seatsSelectedCount.other": "{count} selected",
1515
+ "picker.section": "Section",
1516
+ "picker.secureMore": "Secure more",
1517
+ "picker.secureMoreAndCheckout": "Secure {count} more & checkout",
1518
+ "picker.securedCount": "{count} secured",
1519
+ "picker.seeItIn3d": "See it in 3D",
1520
+ "picker.select": "Select",
1521
+ "picker.selectSeats": "Select seats",
1522
+ "picker.selectTable": "Select table",
1523
+ "picker.selectWholeTable": "Select whole table",
1524
+ "picker.selected": "Selected",
1525
+ "picker.selectingEllipsis": "Selecting\u2026",
1526
+ "picker.showAllSeats": "Show all seats",
1527
+ "picker.showAllTicketTypes": "Show all {count} ticket types",
1528
+ "picker.showFewer": "Show fewer",
1529
+ "picker.showLabelSeatsOnMap": "Show {label} seats on the map",
1530
+ "picker.showTicketPanel": "Show the ticket panel",
1531
+ "picker.sold": "Sold",
1532
+ "picker.soldOutCopy": "No reserved seats are currently available for this event.",
1533
+ "picker.soldOutEyebrow": "This event",
1534
+ "picker.soldOutTitle": "Sold out",
1535
+ "picker.soldSeatExplanation": "This seat has already been booked.",
1536
+ "picker.statusAvailable": "available",
1537
+ "picker.statusOnHold": "on hold",
1538
+ "picker.statusTaken2": "taken",
1539
+ "picker.table": "Table",
1540
+ "picker.tableCapacity": "Table capacity",
1541
+ "picker.tableUpdatedForGuests": "{label} updated for {count} guests.",
1542
+ "picker.temporarilyHeld": "Temporarily held",
1543
+ "picker.ticketPrices": "Ticket prices",
1544
+ "picker.ticketTypeSoldOut": "That ticket type is sold out. Try another ticket type.",
1545
+ "picker.tickets": "Tickets",
1546
+ "picker.ticketsCount.one": "{count} ticket",
1547
+ "picker.ticketsCount.other": "{count} tickets",
1548
+ "picker.toggleColorblindColors": "Toggle colorblind-friendly colors",
1549
+ "picker.total": "Total",
1550
+ "picker.trayHintTapOrBest": "Tap a seat on the map, or let us pick the best available for you.",
1551
+ "picker.trayHintTapOrStanding": "Tap a seat on the map \u2014 or grab standing tickets below.",
1552
+ "picker.undo": "Undo",
1553
+ "picker.upcomingTicketOffer": "Upcoming ticket offer",
1554
+ "picker.updateTable": "Update table",
1555
+ "picker.updatingEllipsis": "Updating\u2026",
1556
+ "picker.venueView": "Venue view",
1557
+ "picker.viewFromHere": "View from here",
1558
+ "picker.viewFromThisSeat": "View from this seat",
1559
+ "picker.wholeTypeWord": "Whole {typeWord}",
1560
+ "picker.willChooseClosestGroup": "We\u2019ll choose the closest available group for you.",
1561
+ "picker.willFindSeatsTogether": "We\u2019ll find {count} seats together.",
1562
+ "picker.yourSeats": "Your seats",
1563
+ "picker.zoomIn": "Zoom in",
1564
+ "picker.zoomOut": "Zoom out",
1565
+ "picker.closeVenueNav": "Close",
1566
+ "picker.levelsAndAreas": "Levels & areas"
1328
1567
  };
1329
1568
 
1330
1569
  // src/i18n/index.ts
@@ -1398,10 +1637,11 @@ var WHEELCHAIR_GLYPH_MIN_PX = 10;
1398
1637
  var FILTERED_WHEELCHAIR_GLYPH_MIN_PX = 14;
1399
1638
  var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
1400
1639
  var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
1401
- var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
1640
+ var SEAT_DETAIL_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
1402
1641
  var PAN_START_SLOP_PX = 8;
1403
1642
  var GHOST_CLICK_MS = 700;
1404
1643
  var ROW_LABEL_CLEARANCE = 2;
1644
+ var FOCUSED_ROW_LABEL_MIN_PX = MIN_VISIBLE_BOOKABLE_LABEL_PX;
1405
1645
  var ROW_LABEL_COLLISION_CELL = 64;
1406
1646
  var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
1407
1647
  var MAX_LABELS = 700;
@@ -1797,6 +2037,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1797
2037
  this.activeFloorId = "";
1798
2038
  /** When true on a multi-floor chart, render ALL floors stacked (3D overview). */
1799
2039
  this.stacked = false;
2040
+ /** Buyer-facing 2D overview: floors are rigidly arranged without altering topology. */
2041
+ this.floorOverview = false;
1800
2042
  /** Interactive node per seat/booth — a Circle for seats, a Rect for booths. */
1801
2043
  this.circleById = /* @__PURE__ */ new Map();
1802
2044
  /** Booth block geometry, keyed by booth id (= the unit's rowId). */
@@ -2052,6 +2294,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2052
2294
  return;
2053
2295
  }
2054
2296
  if (this.pinch && this.pointers.size >= 2) {
2297
+ this.releaseSectionFocusDim();
2055
2298
  const [a, b] = [...this.pointers.values()];
2056
2299
  const dist = Math.hypot(b.x - a.x, b.y - a.y);
2057
2300
  if (this.pinch.startDist < 1) return;
@@ -2070,6 +2313,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2070
2313
  if (!this.panStarted) {
2071
2314
  if (this.moved <= PAN_START_SLOP_PX) return;
2072
2315
  this.panStarted = true;
2316
+ this.releaseSectionFocusDim();
2073
2317
  }
2074
2318
  this.stage.position({
2075
2319
  x: this.stage.x() + (p.x - this.panLast.x),
@@ -2190,13 +2434,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2190
2434
  }
2191
2435
  // ---- ISeatmapRenderer -----------------------------------------------------
2192
2436
  setChart(doc, opts) {
2193
- if (doc !== this.chartDoc) this.stacked = false;
2437
+ if (doc !== this.chartDoc) {
2438
+ this.stacked = false;
2439
+ this.floorOverview = false;
2440
+ }
2194
2441
  this.cancelAssetLoads();
2195
2442
  this.chartDoc = doc;
2196
2443
  this.activeFloorId = opts?.floorId ?? floorsOf(doc)[0].id;
2197
2444
  this.objectFloor.clear();
2198
2445
  if (doc.floors && doc.floors.length > 1) {
2199
- for (const f of doc.floors) for (const o of f.objects) this.objectFloor.set(o.id, f.id);
2446
+ for (const f of doc.floors) {
2447
+ for (const o of f.objects) {
2448
+ this.objectFloor.set(o.id, f.id);
2449
+ if (o.type === "section" && o.logicalSectionId) {
2450
+ this.objectFloor.set(o.logicalSectionId, f.id);
2451
+ }
2452
+ }
2453
+ }
2200
2454
  }
2201
2455
  const view = this.floorView(doc);
2202
2456
  this.focusedId = null;
@@ -2294,7 +2548,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2294
2548
  this.seats = expandChart(view, {
2295
2549
  // A stacked overview is presentation-only and never opens a seat POV. The
2296
2550
  // per-section renderer below still resolves each object's real floor base.
2297
- floorBaseHeightM: this.stacked ? 0 : this.floorBaseHeightMFor()
2551
+ floorBaseHeightM: this.stacked || this.floorOverview ? 0 : this.floorBaseHeightMFor()
2298
2552
  });
2299
2553
  for (const s of this.seats) {
2300
2554
  this.seatById.set(s.id, s);
@@ -2315,14 +2569,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2315
2569
  this.overlayLayer.add(this.focusRing);
2316
2570
  this.zoomToFit();
2317
2571
  }
2318
- /** The chart to render: all floors stacked (3D overview), the active floor, or
2319
- * the whole chart for single-floor charts. */
2572
+ /** The chart to render: authored-coordinate overview, stacked 3D overview,
2573
+ * the active floor, or the whole chart for single-floor charts. */
2320
2574
  floorView(doc) {
2321
2575
  if (!doc.floors || !doc.floors.length) return {
2322
2576
  ...doc,
2323
2577
  referenceImage: void 0,
2324
2578
  backgroundImage: buyerBackgroundImage(doc)
2325
2579
  };
2580
+ if (this.floorOverview && doc.floors.length >= 2) {
2581
+ return {
2582
+ ...layoutFloorOverview(doc),
2583
+ referenceImage: void 0,
2584
+ backgroundImage: buyerBackgroundImage(doc)
2585
+ };
2586
+ }
2326
2587
  if (this.stacked && doc.floors.length >= 2) {
2327
2588
  return {
2328
2589
  ...stackFloors(doc),
@@ -2355,14 +2616,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2355
2616
  const multi = !!this.chartDoc.floors && this.chartDoc.floors.length >= 2;
2356
2617
  if (!multi) return;
2357
2618
  this.stacked = on;
2619
+ if (on) this.floorOverview = false;
2358
2620
  this.setChart(this.chartDoc, { floorId: this.activeFloorId });
2359
2621
  }
2360
2622
  isStacked() {
2361
2623
  return this.stacked;
2362
2624
  }
2625
+ /** Toggle the buyer's combined 2D floor overview. Every floor is translated
2626
+ * as one rigid group for clarity; its authored internal topology is exact. */
2627
+ setFloorOverview(on) {
2628
+ if (!this.chartDoc || on === this.floorOverview) return;
2629
+ const multi = !!this.chartDoc.floors && this.chartDoc.floors.length >= 2;
2630
+ if (!multi) return;
2631
+ this.floorOverview = on;
2632
+ if (on) this.stacked = false;
2633
+ this.setChart(this.chartDoc, { floorId: this.activeFloorId });
2634
+ }
2635
+ isFloorOverview() {
2636
+ return this.floorOverview;
2637
+ }
2363
2638
  /** Switch which floor is shown (2D). Re-renders + re-fits; no-op if unchanged. */
2364
2639
  setActiveFloor(floorId) {
2365
- if (!this.chartDoc || floorId === this.activeFloorId) return;
2640
+ if (!this.chartDoc || floorId === this.activeFloorId && !this.floorOverview && !this.stacked) return;
2641
+ this.floorOverview = false;
2642
+ this.stacked = false;
2366
2643
  this.setChart(this.chartDoc, { floorId });
2367
2644
  }
2368
2645
  /** Floors for the host's switcher (single-floor charts return one synthetic floor). */
@@ -2821,6 +3098,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2821
3098
  const h = this.stage.height();
2822
3099
  const b = this.viewMode === "perspective" && this.perspectiveBounds ? this.perspectiveBounds : this.bounds;
2823
3100
  this.fitScale = Math.min(w / b.width, h / b.height) || 1;
3101
+ if (this.opts.portraitFitCrop && !this.exportMode) {
3102
+ const widthBound = w / b.width <= h / b.height;
3103
+ const projectedH = b.height * this.fitScale;
3104
+ if (widthBound && projectedH < 0.55 * h) {
3105
+ this.fitScale = Math.min(this.fitScale * 1.3, 0.62 * h / b.height);
3106
+ }
3107
+ }
2824
3108
  const s = this.fitScale;
2825
3109
  this.stage.scale({ x: s, y: s });
2826
3110
  this.stage.position({
@@ -2830,6 +3114,18 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2830
3114
  this.afterViewChange();
2831
3115
  this.stage.batchDraw();
2832
3116
  }
3117
+ /**
3118
+ * Reframe after a host/layout resize without throwing away section context.
3119
+ * The stage already represents the usable map area (the picker side panel is
3120
+ * a sibling), so measuring it here naturally respects open/closed panels.
3121
+ */
3122
+ refitCurrentView() {
3123
+ if (this.focusedSectionId && this.sectionBounds(this.focusedSectionId)) {
3124
+ this.focusRegion(this.focusedSectionId, { animate: false });
3125
+ return;
3126
+ }
3127
+ this.zoomToFit();
3128
+ }
2833
3129
  zoomIn() {
2834
3130
  const center = { x: this.stage.width() / 2, y: this.stage.height() / 2 };
2835
3131
  this.zoomAbout(this.stage.scaleX() * _SeatmapRenderer.ZOOM_STEP, center);
@@ -3920,8 +4216,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3920
4216
  }
3921
4217
  /**
3922
4218
  * AXS section-focus: dim + desaturate every other section, draw a calm backdrop
3923
- * panel behind this section's seats, and glide the camera in to frame it (the
3924
- * seat-pick gate below only lets buyers pick once seats are LABEL_SCALE big).
4219
+ * panel behind this section's seats, and glide the camera to a padded fit of
4220
+ * the whole section. A large section may therefore use smaller seat markers,
4221
+ * but it never clips its outer rows merely to meet a fixed detail scale.
3925
4222
  */
3926
4223
  focusSection(id) {
3927
4224
  if (!this.sections.some((section) => section.id === id || section.logicalId === id)) return;
@@ -3930,7 +4227,24 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3930
4227
  this.bgLayer.batchDraw();
3931
4228
  this.overlayLayer.batchDraw();
3932
4229
  this.updateLOD();
3933
- this.focusRegion(id, { minScale: SEAT_FOCUS_SCALE });
4230
+ this.focusRegion(id);
4231
+ }
4232
+ /**
4233
+ * Release only the focus dim + backdrop, keeping the focused-section state
4234
+ * (summary card, adjacent-section navigation, LOD) intact. Called on the
4235
+ * buyer's first manual pan/pinch/wheel after a section glide: the dim reads
4236
+ * as "everything else is disabled", but every seat outside the focus was
4237
+ * always hit-testable — free roaming across sections is the product
4238
+ * behaviour, the glide is only an entrance.
4239
+ */
4240
+ releaseSectionFocusDim() {
4241
+ if (!this.focusBackdrop && !this.focusDimOverlay) return;
4242
+ this.focusBackdrop?.destroy();
4243
+ this.focusBackdrop = null;
4244
+ this.focusDimOverlay?.destroy();
4245
+ this.focusDimOverlay = null;
4246
+ this.bgLayer.batchDraw();
4247
+ this.overlayLayer.batchDraw();
3934
4248
  }
3935
4249
  /** Clear an AXS section focus — restore full-bowl brightness + drop the backdrop. */
3936
4250
  clearSectionFocus() {
@@ -4068,6 +4382,44 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4068
4382
  if (!Number.isFinite(minX)) return { x: 0, y: 0, width: 1, height: 1 };
4069
4383
  return { x: minX, y: minY, width: Math.max(1, maxX - minX), height: Math.max(1, maxY - minY) };
4070
4384
  }
4385
+ /** Live minimap geometry comes from the rendered scene, so a content-aware
4386
+ * all-floor layout cannot collapse back onto the source coordinate stack. */
4387
+ getMinimapSnapshot() {
4388
+ const sections = this.sections.map((section) => {
4389
+ const inactive = this.isSectionClosed(section) || section.free <= 0;
4390
+ const selected = section.memberIds.some((id) => this.selection.has(id));
4391
+ const active2 = this.focusedSectionId === section.id || this.focusedSectionId === section.logicalId;
4392
+ return {
4393
+ id: section.logicalId,
4394
+ label: section.label,
4395
+ outline: section.outline.map((point) => ({ ...point })),
4396
+ ...this.objectFloor.get(section.id) ? { floorId: this.objectFloor.get(section.id) } : {},
4397
+ state: selected ? "selected" : inactive ? "inactive" : "available",
4398
+ active: active2
4399
+ };
4400
+ });
4401
+ return {
4402
+ sections,
4403
+ seats: sections.length ? [] : this.seats.map((seat) => ({
4404
+ x: seat.x,
4405
+ y: seat.y,
4406
+ categoryKey: seat.categoryKey,
4407
+ state: this.selection.has(seat.id) ? "selected" : this.statusById.get(seat.id) === "free" ? "available" : "inactive"
4408
+ }))
4409
+ };
4410
+ }
4411
+ /** Minimap drag/click navigation: preserve zoom and move one world point to
4412
+ * the viewport centre. Works for flat and projected views via worldToScreen. */
4413
+ panToWorld(point) {
4414
+ this.cancelGlide();
4415
+ const screen = this.worldToScreen(point);
4416
+ this.stage.position({
4417
+ x: this.stage.x() + this.stage.width() / 2 - screen.x,
4418
+ y: this.stage.y() + this.stage.height() / 2 - screen.y
4419
+ });
4420
+ this.afterViewChange();
4421
+ this.stage.batchDraw();
4422
+ }
4071
4423
  /** Legend hover: highlight one category (dim the rest), or null to clear. */
4072
4424
  setCategoryHighlight(key) {
4073
4425
  if (this.categoryHighlight === key) return;
@@ -4836,7 +5188,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4836
5188
  blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
4837
5189
  zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
4838
5190
  }
4839
- const sectionOverview = scale < CACHE_THRESHOLD;
5191
+ if (this.floorOverview) {
5192
+ blockT = 0;
5193
+ zoneT = 0;
5194
+ }
5195
+ if (this.focusedSectionId) {
5196
+ blockT = 0;
5197
+ zoneT = 0;
5198
+ }
5199
+ const sectionOverview = !this.floorOverview && !this.focusedSectionId && scale < CACHE_THRESHOLD;
4840
5200
  if (sectionOverview) blockT = 1;
4841
5201
  if (!this.zones.length) zoneT = 0;
4842
5202
  const deferPerspectiveSeatReveal = this.viewMode === "perspective" && this.glideInProgress && this.seats.length > 2500;
@@ -4981,8 +5341,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4981
5341
  fitSectionRungLabels(sec, sx) {
4982
5342
  const paddingPx = 8;
4983
5343
  sec.subLabelFits = false;
4984
- const maxPx = SECTION_LABEL_PX * sec.labelScale;
4985
- const minPx = MIN_SECTION_LABEL_PX * sec.labelScale;
5344
+ const maxPx = SECTION_LABEL_PX * sec.labelScale * this.uiScale();
5345
+ const minPx = Math.min(MIN_SECTION_LABEL_PX * sec.labelScale, maxPx);
4986
5346
  for (let fontPx = maxPx; fontPx >= minPx; fontPx -= 1) {
4987
5347
  for (const rotation of [0, -90]) {
4988
5348
  sec.nameLabel.rotation(rotation);
@@ -5008,13 +5368,24 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5008
5368
  sec.nameLabel.rotation(0);
5009
5369
  sec.nameLabelFits = false;
5010
5370
  }
5371
+ /**
5372
+ * Screen-constant chrome must still respect the screen it is constant on:
5373
+ * an 18px zone cap sized for a 900px designer canvas eats a 360px phone map
5374
+ * (owner report, 2026-08-14). Scales the fixed-px chrome down to 66% on the
5375
+ * smallest containers; never up.
5376
+ */
5377
+ uiScale() {
5378
+ const w = this.stage?.width() ?? 900;
5379
+ return Math.min(1, Math.max(0.66, w / 900));
5380
+ }
5011
5381
  /** Size one screen-constant zone name/price pill around its shared anchor. */
5012
5382
  sizeZonePill(zone, sx) {
5013
- const padX = 10 / sx;
5014
- const padY = 6 / sx;
5015
- const gap = zone.sub ? 3 / sx : 0;
5016
- this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, zone.anchor.y);
5017
- if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX / sx, zone.anchor.y);
5383
+ const k = this.uiScale();
5384
+ const padX = 10 * k / sx;
5385
+ const padY = 6 * k / sx;
5386
+ const gap = zone.sub ? 3 * k / sx : 0;
5387
+ this.sizeLabel(zone.label, ZONE_LABEL_PX * k / sx, zone.anchor.y);
5388
+ if (zone.sub) this.sizeLabel(zone.sub, ZONE_SUB_PX * k / sx, zone.anchor.y);
5018
5389
  const width = Math.max(zone.label.width(), zone.sub?.width() ?? 0) + padX * 2;
5019
5390
  const height = zone.label.height() + (zone.sub ? gap + zone.sub.height() : 0) + padY * 2;
5020
5391
  zone.label.y(zone.anchor.y - (zone.sub ? (gap + zone.sub.height()) / 2 : 0));
@@ -5022,8 +5393,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5022
5393
  zone.back.position(zone.anchor);
5023
5394
  zone.back.size({ width, height });
5024
5395
  zone.back.offset({ x: width / 2, y: height / 2 });
5025
- zone.back.cornerRadius(7 / sx);
5396
+ zone.back.cornerRadius(7 * this.uiScale() / sx);
5026
5397
  zone.back.strokeWidth(1 / sx);
5398
+ const abs = zone.back.getAbsolutePosition();
5399
+ const minScreenY = 62 + height * sx / 2;
5400
+ if (abs.y < minScreenY) {
5401
+ const dy = (minScreenY - abs.y) / sx;
5402
+ zone.label.y(zone.label.y() + dy);
5403
+ if (zone.sub) zone.sub.y(zone.sub.y() + dy);
5404
+ zone.back.y(zone.back.y() + dy);
5405
+ }
5027
5406
  }
5028
5407
  /**
5029
5408
  * Map a container-relative screen point back to world coords. Inverts the
@@ -5248,7 +5627,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5248
5627
  * must fall through and pick.
5249
5628
  */
5250
5629
  sectionBounds(id) {
5251
- const bounds2 = this.sections.filter((section) => section.id === id || section.logicalId === id).map((section) => {
5630
+ const matching = this.sections.filter((section) => section.id === id || section.logicalId === id);
5631
+ const bounds2 = matching.map((section) => {
5252
5632
  if (this.viewMode === "perspective" && section.perspectiveAffine) {
5253
5633
  return polyBounds(section.outline.map((point) => this.projectedSectionPoint(section, point)));
5254
5634
  }
@@ -5260,11 +5640,77 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5260
5640
  return polyBounds(points);
5261
5641
  });
5262
5642
  if (!bounds2.length) return null;
5263
- const left = Math.min(...bounds2.map((box) => box.x));
5264
- const top = Math.min(...bounds2.map((box) => box.y));
5265
- const right = Math.max(...bounds2.map((box) => box.x + box.width));
5266
- const bottom = Math.max(...bounds2.map((box) => box.y + box.height));
5267
- return { x: left, y: top, width: right - left, height: bottom - top };
5643
+ let left = Math.min(...bounds2.map((box) => box.x));
5644
+ let top = Math.min(...bounds2.map((box) => box.y));
5645
+ let right = Math.max(...bounds2.map((box) => box.x + box.width));
5646
+ let bottom = Math.max(...bounds2.map((box) => box.y + box.height));
5647
+ const memberIds = new Set(matching.flatMap((section) => section.memberIds));
5648
+ for (const seatId of memberIds) {
5649
+ const seat = this.seatById.get(seatId);
5650
+ if (!seat) continue;
5651
+ const point = this.renderedSeatPoint(seat);
5652
+ left = Math.min(left, point.x - this.seatR);
5653
+ top = Math.min(top, point.y - this.seatR);
5654
+ right = Math.max(right, point.x + this.seatR);
5655
+ bottom = Math.max(bottom, point.y + this.seatR);
5656
+ }
5657
+ const includeBox = (box) => {
5658
+ if (![box.x, box.y, box.width, box.height].every(Number.isFinite)) return;
5659
+ left = Math.min(left, box.x);
5660
+ top = Math.min(top, box.y);
5661
+ right = Math.max(right, box.x + box.width);
5662
+ bottom = Math.max(bottom, box.y + box.height);
5663
+ };
5664
+ const belongsToSection = (box) => {
5665
+ const centre = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
5666
+ return matching.some((section) => pointInPolygonWithHoles(centre, section.outline, section.holes));
5667
+ };
5668
+ for (const { node, backdrop } of this.freeTextById.values()) {
5669
+ const nodeBox = node.getClientRect({ relativeTo: this.bgLayer, skipShadow: true });
5670
+ if (!belongsToSection(nodeBox)) continue;
5671
+ includeBox(nodeBox);
5672
+ if (backdrop) includeBox(backdrop.getClientRect({ relativeTo: this.bgLayer, skipShadow: true }));
5673
+ }
5674
+ for (const { node } of this.iconNodeById.values()) {
5675
+ const box = node.getClientRect({ relativeTo: this.bgLayer, skipShadow: true });
5676
+ if (belongsToSection(box)) includeBox(box);
5677
+ }
5678
+ const occupiedText = new WorldBoxIndex();
5679
+ for (const { node } of this.freeTextById.values()) {
5680
+ const box = node.getClientRect({ relativeTo: this.bgLayer, skipShadow: true, skipStroke: true });
5681
+ occupiedText.insert({
5682
+ x: box.x - ROW_LABEL_CLEARANCE,
5683
+ y: box.y - ROW_LABEL_CLEARANCE,
5684
+ width: box.width + ROW_LABEL_CLEARANCE * 2,
5685
+ height: box.height + ROW_LABEL_CLEARANCE * 2
5686
+ });
5687
+ }
5688
+ for (const plan of this.rowLabelPlan) {
5689
+ const relevant = plan.focusKeys?.some((key) => key === id || matching.some((section) => key === section.id || key === section.logicalId || key === section.zone)) ?? false;
5690
+ const label = new Text({
5691
+ text: plan.text,
5692
+ fontSize: plan.fontSize,
5693
+ fontStyle: "700",
5694
+ fontFamily: this.labelFont(),
5695
+ rotation: plan.rotation,
5696
+ listening: false
5697
+ });
5698
+ label.offsetX(label.width() / 2);
5699
+ label.offsetY(label.height() / 2);
5700
+ const resolved = this.resolveAutomaticRowLabelPosition(label, plan, occupiedText);
5701
+ const box = this.rowLabelBox(label, resolved);
5702
+ if (relevant) includeBox(box);
5703
+ occupiedText.insert(box);
5704
+ label.destroy();
5705
+ }
5706
+ const span = Math.max(right - left, bottom - top);
5707
+ const padding = Math.max(this.seatR * 2, Math.min(24, span * 0.025));
5708
+ return {
5709
+ x: left - padding,
5710
+ y: top - padding,
5711
+ width: Math.max(1, right - left + padding * 2),
5712
+ height: Math.max(1, bottom - top + padding * 2)
5713
+ };
5268
5714
  }
5269
5715
  sectionFrameScale(id) {
5270
5716
  const b = this.sectionBounds(id);
@@ -5275,7 +5721,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5275
5721
  const margin = 1.12;
5276
5722
  if (b.width <= 0 || b.height <= 0) return this.stage.scaleX();
5277
5723
  const frameScale = Math.min(w / (b.width * margin), h / (b.height * margin));
5278
- return clamp(Math.max(frameScale, SEAT_FOCUS_SCALE), min, max);
5724
+ return clamp(frameScale, min, max);
5279
5725
  }
5280
5726
  /**
5281
5727
  * Resolve a seat tap: honour the 3D deck-drill and the AXS seat-pick gate,
@@ -5288,7 +5734,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5288
5734
  * fall through and PICK. This is the invariant: seats are never unpickable.
5289
5735
  */
5290
5736
  handleSeatTap(id) {
5291
- if (this.stacked && this.opts.onDeckTap) {
5737
+ if ((this.stacked || this.floorOverview) && this.opts.onDeckTap) {
5292
5738
  const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
5293
5739
  if (floorId) {
5294
5740
  this.opts.onDeckTap(floorId);
@@ -5363,6 +5809,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5363
5809
  });
5364
5810
  this.stage.on("wheel", (e) => {
5365
5811
  e.evt.preventDefault();
5812
+ this.releaseSectionFocusDim();
5366
5813
  const r = this.container.getBoundingClientRect();
5367
5814
  const pointer = { x: e.evt.clientX - r.left, y: e.evt.clientY - r.top };
5368
5815
  const factor = Math.exp(-e.evt.deltaY * 2e-3);
@@ -5379,8 +5826,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5379
5826
  if (near) this.handleSeatTap(near);
5380
5827
  return;
5381
5828
  }
5382
- if (this.stacked && this.opts.onDeckTap) {
5383
- const floorId = this.deckFloorAt();
5829
+ if ((this.stacked || this.floorOverview) && this.opts.onDeckTap) {
5830
+ const sectionId = this.floorOverview ? this.sectionAt(pointer) : null;
5831
+ const floorId = (sectionId ? this.objectFloor.get(sectionId) : null) ?? this.deckFloorAt();
5384
5832
  if (floorId) {
5385
5833
  this.opts.onDeckTap(floorId);
5386
5834
  return;
@@ -5778,6 +6226,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5778
6226
  ...hiddenReason ? { hiddenReason } : {}
5779
6227
  };
5780
6228
  });
6229
+ const rowLabels = this.labelGroup.find((node) => node.getAttr("rowLabel") === true).filter((node) => node instanceof Text).map((node) => {
6230
+ const screenBounds = node.getClientRect({
6231
+ skipShadow: true,
6232
+ skipStroke: true
6233
+ });
6234
+ const outside = screenBounds.x + screenBounds.width < 0 || screenBounds.x > viewport.width || screenBounds.y + screenBounds.height < 0 || screenBounds.y > viewport.height;
6235
+ const ink = node.fill();
6236
+ return {
6237
+ text: node.text(),
6238
+ visible: node.isVisible() && !outside,
6239
+ renderedFontPx: rounded(node.fontSize() * effectiveScale),
6240
+ ink: typeof ink === "string" ? ink : "",
6241
+ ...!outside ? {
6242
+ screenBox: {
6243
+ x: rounded(screenBounds.x),
6244
+ y: rounded(screenBounds.y),
6245
+ width: rounded(screenBounds.width),
6246
+ height: rounded(screenBounds.height)
6247
+ }
6248
+ } : {}
6249
+ };
6250
+ });
5781
6251
  const palette = overviewPalette(this.canvasBackground);
5782
6252
  const neutralSectionFills = new Set(
5783
6253
  ["normal", "nearly-gone", "sold-out", "closed"].map((state) => overviewStateFill(palette, state).toLowerCase())
@@ -5822,7 +6292,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5822
6292
  labels,
5823
6293
  gaAreas,
5824
6294
  hierarchyLabels,
5825
- freeTextLabels
6295
+ freeTextLabels,
6296
+ rowLabels
5826
6297
  };
5827
6298
  }
5828
6299
  /** Jump the camera to a rung's zoom band (glided). */
@@ -5834,7 +6305,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5834
6305
  }
5835
6306
  const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(
5836
6307
  this.seatLabelTargetScale() * 1.05,
5837
- SEAT_FOCUS_SCALE,
6308
+ SEAT_DETAIL_SCALE,
5838
6309
  CACHE_THRESHOLD * 1.3
5839
6310
  );
5840
6311
  const w = this.stage.width();
@@ -6121,14 +6592,25 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6121
6592
  for (const { objectId, node, backdrop, categoryKey, kind } of this.freeTextById.values()) {
6122
6593
  const gaDimmed = categoryKey != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaCategoryDimmed(categoryKey);
6123
6594
  const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
6124
- const visible = !gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale);
6595
+ const focusedAnnotation = kind === "free-text" && this.nodeBelongsToFocusedSection(node);
6596
+ const visible = !gaDimmed && !gaOverviewHidden && (focusedAnnotation || isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
6125
6597
  node.visible(visible);
6126
6598
  backdrop?.visible(visible);
6127
6599
  }
6128
6600
  for (const { node, fontSize } of this.iconNodeById.values()) {
6129
- node.visible(isBookableLabelLegibleAtScale(fontSize, effectiveScale));
6601
+ node.visible(this.nodeBelongsToFocusedSection(node) || isBookableLabelLegibleAtScale(fontSize, effectiveScale));
6130
6602
  }
6131
6603
  }
6604
+ /** Geometric association only: importer data remains unchanged. */
6605
+ nodeBelongsToFocusedSection(node) {
6606
+ const focus = this.focusedSectionId;
6607
+ if (!focus) return false;
6608
+ const matching = this.sections.filter((section) => section.id === focus || section.logicalId === focus || section.zone === focus);
6609
+ if (!matching.length) return false;
6610
+ const box = node.getClientRect({ relativeTo: this.bgLayer, skipShadow: true });
6611
+ const centre = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
6612
+ return matching.some((section) => pointInPolygonWithHoles(centre, section.outline, section.holes));
6613
+ }
6132
6614
  /** OV-45(a): resolve authored row-letter labels for the buyer/preview map, in
6133
6615
  * world space, matching the Designer's placement contract (start/end/both/none
6134
6616
  * preset, a free-drag `position` that wins over the preset, rotation, and
@@ -6138,18 +6620,25 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6138
6620
  this.rowLabelPlan = [];
6139
6621
  const background = this.theme.background ?? "#0e1117";
6140
6622
  const seatsByRow = /* @__PURE__ */ new Map();
6623
+ const addSeat = (key, seat) => {
6624
+ const list = seatsByRow.get(key) ?? [];
6625
+ list.push(seat);
6626
+ seatsByRow.set(key, list);
6627
+ };
6141
6628
  for (const seat of this.seats) {
6142
6629
  if (seat.kind === "booth") continue;
6143
6630
  const key = seat.logicalRowId ?? seat.rowId;
6144
- (seatsByRow.get(key) ?? seatsByRow.set(key, []).get(key)).push(seat);
6631
+ addSeat(key, seat);
6632
+ if (key !== seat.rowId) addSeat(seat.rowId, seat);
6145
6633
  }
6146
6634
  const seatOrder = (seat) => seat.logicalSeatIndex ?? Number(seat.id.split(":").pop() ?? 0);
6147
6635
  for (const obj of view.objects) {
6148
6636
  if (obj.type !== "row") continue;
6149
- if (obj.segmentedRow && obj.segmentedRow.componentIndex !== 0) continue;
6150
- const presentation = obj.segmentedRow?.labelPresentation ?? obj.labelPresentation;
6637
+ const repeatOnComponents = obj.segmentedRow?.repeatLabelOnComponents === true;
6638
+ if (obj.segmentedRow && !repeatOnComponents && obj.segmentedRow.componentIndex !== 0) continue;
6639
+ const presentation = repeatOnComponents ? obj.labelPresentation : obj.segmentedRow?.labelPresentation ?? obj.labelPresentation;
6151
6640
  if (presentation?.visible === false || presentation?.positionPreset === "none") continue;
6152
- const key = obj.segmentedRow ? obj.segmentedRow.groupId : obj.id;
6641
+ const key = obj.segmentedRow && !repeatOnComponents ? obj.segmentedRow.groupId : obj.id;
6153
6642
  const seats = (seatsByRow.get(key) ?? []).slice().sort((a, b) => seatOrder(a) - seatOrder(b));
6154
6643
  if (!seats.length) continue;
6155
6644
  const first = seats[0];
@@ -6164,6 +6653,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6164
6653
  const rotation = presentation?.rotation ?? 0;
6165
6654
  const text = obj.segmentedRow?.displayLabel ?? obj.displayLabel ?? obj.label;
6166
6655
  const opacity = this.objectFilteredOut(obj) ? 0.15 : 1;
6656
+ const containingSection = this.seatSection.get(first.id);
6657
+ const focusKeys = containingSection ? [...new Set([containingSection.id, containingSection.logicalId, containingSection.zone].filter(
6658
+ (value) => Boolean(value)
6659
+ ))] : void 0;
6167
6660
  const push = (x, y, automaticAway) => this.rowLabelPlan.push({
6168
6661
  text,
6169
6662
  x,
@@ -6172,6 +6665,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6172
6665
  fontSize,
6173
6666
  ink,
6174
6667
  opacity,
6668
+ ...focusKeys?.length ? { focusKeys } : {},
6175
6669
  ...automaticAway ? { automaticAway } : {}
6176
6670
  });
6177
6671
  if (presentation?.position) {
@@ -6191,6 +6685,18 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6191
6685
  push(last.x + away.x * gap, last.y + away.y * gap, away);
6192
6686
  }
6193
6687
  }
6688
+ const mergeRadius = (this.seatR + 12) * 2.2;
6689
+ const merged = [];
6690
+ for (const entry of this.rowLabelPlan) {
6691
+ const partner = merged.find((candidate) => candidate.text === entry.text && Math.hypot(candidate.x - entry.x, candidate.y - entry.y) <= mergeRadius);
6692
+ if (partner) {
6693
+ partner.x = (partner.x + entry.x) / 2;
6694
+ partner.y = (partner.y + entry.y) / 2;
6695
+ continue;
6696
+ }
6697
+ merged.push(entry);
6698
+ }
6699
+ this.rowLabelPlan = merged;
6194
6700
  }
6195
6701
  /** Axis-aligned world bounds of a centred, potentially rotated row label. */
6196
6702
  rowLabelBox(label, position, padding = ROW_LABEL_CLEARANCE) {
@@ -6267,12 +6773,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6267
6773
  this.labelGroup.destroyChildren();
6268
6774
  this.labelLiftGroups.clear();
6269
6775
  this.seatLabelById.clear();
6270
- if (!show) {
6271
- this.overlayLayer.batchDraw();
6272
- return;
6273
- }
6274
6776
  let count = 0;
6275
- for (const seat of this.visibleSeatCandidates()) {
6777
+ if (show) for (const seat of this.visibleSeatCandidates()) {
6276
6778
  if (seat.kind === "booth") continue;
6277
6779
  const screen = this.worldToScreen(seat);
6278
6780
  if (screen.x < -20 || screen.x > this.stage.width() + 20 || screen.y < -20 || screen.y > this.stage.height() + 20) continue;
@@ -6363,13 +6865,16 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6363
6865
  });
6364
6866
  }
6365
6867
  for (const rl of this.rowLabelPlan) {
6868
+ const focused = this.focusedSectionId != null && rl.focusKeys?.includes(this.focusedSectionId);
6869
+ const renderedFontSize = focused ? Math.max(rl.fontSize, FOCUSED_ROW_LABEL_MIN_PX / Math.max(effectiveScale, 1e-4)) : rl.fontSize;
6870
+ if (!focused && !isBookableLabelLegibleAtScale(renderedFontSize, effectiveScale)) continue;
6366
6871
  const screen = this.worldToScreen({ x: rl.x, y: rl.y });
6367
6872
  if (screen.x < -40 || screen.x > this.stage.width() + 40 || screen.y < -40 || screen.y > this.stage.height() + 40) continue;
6368
6873
  const t2 = new Text({
6369
6874
  x: rl.x,
6370
6875
  y: rl.y,
6371
6876
  text: rl.text,
6372
- fontSize: rl.fontSize,
6877
+ fontSize: renderedFontSize,
6373
6878
  fontStyle: "700",
6374
6879
  fontFamily: this.labelFont(),
6375
6880
  fill: rl.ink,
@@ -6398,7 +6903,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6398
6903
  const h = this.container.clientHeight || 1;
6399
6904
  if (w === this.stage.width() && h === this.stage.height()) return;
6400
6905
  this.stage.size({ width: w, height: h });
6401
- this.zoomToFit();
6906
+ this.refitCurrentView();
6402
6907
  }
6403
6908
  startFpsLoop() {
6404
6909
  const tick = (now) => {
@@ -6556,6 +7061,99 @@ function pickBestAvailable(seats, available, opts) {
6556
7061
  return { labels: ranked.slice(0, qty).map((r) => r.seat.label) };
6557
7062
  }
6558
7063
 
7064
+ // src/core/ticketRestrictions.ts
7065
+ function ticketOptionNotApplicableIssue(labels) {
7066
+ return {
7067
+ code: "ticket_option_not_applicable",
7068
+ labels,
7069
+ message: "That ticket choice is not available for the selected seat."
7070
+ };
7071
+ }
7072
+ function ticketTierAppliesToSeat(tier, seat) {
7073
+ const applicability = tier.applicability;
7074
+ if (!applicability) return true;
7075
+ if (applicability.seatLabels?.length && !applicability.seatLabels.includes(seat.label)) return false;
7076
+ if (applicability.accessibility?.length && !applicability.accessibility.some((key) => seat.accessibility?.includes(key))) return false;
7077
+ return true;
7078
+ }
7079
+ function applicableTicketTiers(doc, seat) {
7080
+ return (doc.categories.find((category) => category.key === seat.categoryKey)?.tiers ?? []).filter((tier) => ticketTierAppliesToSeat(tier, seat));
7081
+ }
7082
+ function resolveTicketTierForSeat(doc, seat, tierId) {
7083
+ const tiers = doc.categories.find((category) => category.key === seat.categoryKey)?.tiers;
7084
+ if (!tiers?.length) return { kind: "none" };
7085
+ const tier = tierId == null ? tiers.find((candidate) => ticketTierAppliesToSeat(candidate, seat)) : tiers.find((candidate) => candidate.id === tierId && ticketTierAppliesToSeat(candidate, seat));
7086
+ return tier ? { kind: "resolved", tier } : { kind: "invalid" };
7087
+ }
7088
+ function sameLogicalRow(a, b) {
7089
+ return (a.logicalRowId ?? a.rowId) === (b.logicalRowId ?? b.rowId);
7090
+ }
7091
+ function authoredIndex(seat) {
7092
+ if (Number.isInteger(seat.logicalSeatIndex)) return seat.logicalSeatIndex;
7093
+ const raw = seat.id.slice(seat.id.lastIndexOf(":") + 1);
7094
+ const index = Number(raw);
7095
+ return Number.isInteger(index) ? index : void 0;
7096
+ }
7097
+ function observablyAdjacent(a, b) {
7098
+ if (!sameLogicalRow(a, b)) return false;
7099
+ const aIndex = authoredIndex(a);
7100
+ const bIndex = authoredIndex(b);
7101
+ return aIndex != null && bIndex != null && Math.abs(aIndex - bIndex) === 1;
7102
+ }
7103
+ function companionSelectionIssue(doc, selections) {
7104
+ if (!selections.length) return null;
7105
+ const byLabel = new Map(expandChart(doc, { resolveEyeHeights: false }).map((seat) => [seat.label, seat]));
7106
+ const selected = selections.flatMap((selection) => {
7107
+ const seat = byLabel.get(selection.label);
7108
+ return seat ? [{ selection, seat }] : [];
7109
+ });
7110
+ const wheelchair = selected.map(({ seat }) => seat).filter((seat) => seat.accessibility?.includes("wheelchair"));
7111
+ const unpaired = [];
7112
+ for (const { selection, seat } of selected) {
7113
+ const resolution = resolveTicketTierForSeat(doc, seat, selection.tierId);
7114
+ const tier = resolution.kind === "resolved" ? resolution.tier : void 0;
7115
+ const isCompanion = seat.accessibility?.includes("companion") || tier?.restriction === "companion";
7116
+ if (isCompanion && !wheelchair.some((candidate) => candidate.id !== seat.id && observablyAdjacent(seat, candidate))) {
7117
+ unpaired.push(seat.label);
7118
+ }
7119
+ }
7120
+ return unpaired.length ? {
7121
+ code: "companion_pair_required",
7122
+ labels: unpaired,
7123
+ message: "Select the adjacent wheelchair place with each companion ticket."
7124
+ } : null;
7125
+ }
7126
+ function ticketSelectionIssue(doc, selections) {
7127
+ if (!selections.length) return null;
7128
+ const byLabel = new Map(expandChart(doc, { resolveEyeHeights: false }).map((seat) => [seat.label, seat]));
7129
+ const notForSaleCategories = new Set(
7130
+ doc.categories.filter((category) => category.notForSale).map((category) => category.key)
7131
+ );
7132
+ const notForSale = [];
7133
+ const inapplicable = [];
7134
+ for (const selection of selections) {
7135
+ const seat = byLabel.get(selection.label);
7136
+ if (!seat) continue;
7137
+ if (notForSaleCategories.has(seat.categoryKey)) {
7138
+ notForSale.push(seat.label);
7139
+ continue;
7140
+ }
7141
+ const resolution = resolveTicketTierForSeat(doc, seat, selection.tierId);
7142
+ if (resolution.kind === "invalid") inapplicable.push(seat.label);
7143
+ }
7144
+ if (notForSale.length) {
7145
+ return {
7146
+ code: "not_for_sale",
7147
+ labels: notForSale,
7148
+ message: "These seats are not for sale."
7149
+ };
7150
+ }
7151
+ if (inapplicable.length) {
7152
+ return ticketOptionNotApplicableIssue(inapplicable);
7153
+ }
7154
+ return companionSelectionIssue(doc, selections);
7155
+ }
7156
+
6559
7157
  // src/core/tableInventory.ts
6560
7158
  var GroupedTableProjectionError = class extends Error {
6561
7159
  constructor(objectId, message) {
@@ -6610,6 +7208,15 @@ function groupedTableInventory(doc, modelVersion) {
6610
7208
  // src/picker/PickerController.ts
6611
7209
  var DEFAULT_MAX_SELECTION = 10;
6612
7210
  var MAX_BACKOFF_MS = 15e3;
7211
+ var PickerSelectionError = class extends Error {
7212
+ constructor(issue) {
7213
+ super(issue.message);
7214
+ this.status = 422;
7215
+ this.name = "PickerSelectionError";
7216
+ this.reason = issue.code;
7217
+ this.labels = issue.labels;
7218
+ }
7219
+ };
6613
7220
  var MAP_THEME_KEYS = ["background", "rowLabelColor", "textColor", "selectionColor"];
6614
7221
  function sameMapTheme(a, b) {
6615
7222
  if (!a || !b) return !a && !b;
@@ -6739,7 +7346,9 @@ var PickerController = class {
6739
7346
  if (!r) return true;
6740
7347
  const selected = r.getSelection().map((seat) => seat.id);
6741
7348
  const floorId = this.getActiveFloorId();
7349
+ const floorOverview = r.isFloorOverview?.() ?? false;
6742
7350
  r.setChart(this.visibleDoc(), floorId ? { floorId } : void 0);
7351
+ if (floorOverview) r.setFloorOverview?.(true);
6743
7352
  if (this.opts.colorblindSafe) r.setColorblindSafe?.(true);
6744
7353
  if (this.closedSections.size) r.setClosedSections?.([...this.closedSections]);
6745
7354
  this.applySeatsMap(Object.fromEntries(this.liveStatuses), this.liveDefault);
@@ -6751,7 +7360,9 @@ var PickerController = class {
6751
7360
  const next = ids.filter((x) => typeof x === "string");
6752
7361
  if (next.length === this.hidden.size && next.every((id) => this.hidden.has(id))) return false;
6753
7362
  this.hidden = new Set(next);
7363
+ const floorOverview = this.renderer?.isFloorOverview?.() ?? false;
6754
7364
  this.renderer?.setChart(this.visibleDoc());
7365
+ if (floorOverview) this.renderer?.setFloorOverview?.(true);
6755
7366
  return true;
6756
7367
  }
6757
7368
  /** Adopt a new closed-section set; restyle (grey + non-pickable) if it differs.
@@ -6826,16 +7437,22 @@ var PickerController = class {
6826
7437
  /** Fetch chart, build label maps, mount the renderer, seed statuses, go live. */
6827
7438
  async render(host) {
6828
7439
  if (this.renderer) return null;
7440
+ const renderStartedAt = performance.now();
7441
+ const profile = {};
7442
+ const duration = (startedAt) => Math.round((performance.now() - startedAt) * 100) / 100;
6829
7443
  this.closed = false;
6830
7444
  let res;
7445
+ const chartStartedAt = performance.now();
6831
7446
  try {
6832
7447
  res = await this.api.chart(this.key);
6833
7448
  } catch (err) {
6834
7449
  this.emitError(err);
6835
7450
  return null;
6836
7451
  }
7452
+ profile.loadChart = duration(chartStartedAt);
6837
7453
  if (this.closed) return null;
6838
7454
  this._doc = res.doc;
7455
+ const normalizeStartedAt = performance.now();
6839
7456
  try {
6840
7457
  const groups = groupedTableInventory(
6841
7458
  res.doc,
@@ -6900,13 +7517,16 @@ var PickerController = class {
6900
7517
  this.labelToId.set(group.label, representative.id);
6901
7518
  this.labelToSeat.set(group.label, representative);
6902
7519
  }
7520
+ profile.normalizeBuyerModel = duration(normalizeStartedAt);
6903
7521
  const currency = res.event.currency ?? this.opts.currency;
6904
7522
  this.currency = currency ?? "USD";
7523
+ const rendererStartedAt = performance.now();
6905
7524
  const renderer = createRenderer(host, {
6906
7525
  // Group chairs are selected together for paint, but count as one logical
6907
7526
  // inventory line. The controller enforces the guest-weighted cap.
6908
7527
  maxSelection: this.rendererSelectionCap(),
6909
7528
  confirmSelection: this.opts.confirmSelection,
7529
+ portraitFitCrop: true,
6910
7530
  currency,
6911
7531
  onSelect: (seat) => {
6912
7532
  this.handleRendererSelect(seat);
@@ -6927,6 +7547,7 @@ var PickerController = class {
6927
7547
  onDeckTap: (floorId) => this.handleDeckTap(floorId),
6928
7548
  onFps: this.opts.onFps
6929
7549
  });
7550
+ profile.createRenderer = duration(rendererStartedAt);
6930
7551
  if (this.closed) {
6931
7552
  renderer.destroy();
6932
7553
  return null;
@@ -6935,6 +7556,7 @@ var PickerController = class {
6935
7556
  let seats = null;
6936
7557
  let seatsDefault = "free";
6937
7558
  let closedIds = [];
7559
+ const statusesStartedAt = performance.now();
6938
7560
  try {
6939
7561
  const objs = await this.api.objects(this.key);
6940
7562
  this.hidden = new Set(objs.hidden ?? []);
@@ -6943,18 +7565,30 @@ var PickerController = class {
6943
7565
  seatsDefault = objs.default ?? "free";
6944
7566
  } catch {
6945
7567
  }
7568
+ profile.loadStatuses = duration(statusesStartedAt);
6946
7569
  if (this.closed) {
6947
7570
  renderer.destroy();
6948
7571
  this.renderer = null;
6949
7572
  return null;
6950
7573
  }
7574
+ const sceneStartedAt = performance.now();
6951
7575
  renderer.setChart(this.visibleDoc());
7576
+ if ((res.doc.floors?.length ?? 0) > 1) renderer.setFloorOverview?.(true);
6952
7577
  if (this.opts.colorblindSafe) renderer.setColorblindSafe?.(true);
7578
+ profile.buildScene = duration(sceneStartedAt);
6953
7579
  this.closedSections = new Set(closedIds);
7580
+ const initialStateStartedAt = performance.now();
6954
7581
  if (closedIds.length) renderer.setClosedSections?.(closedIds);
6955
7582
  if (seats) this.applySeatsMap(seats, seatsDefault);
7583
+ profile.applyInitialState = duration(initialStateStartedAt);
7584
+ const firstPaintStartedAt = performance.now();
7585
+ renderer.forceDraw();
7586
+ profile.firstPaint = duration(firstPaintStartedAt);
7587
+ const realtimeStartedAt = performance.now();
6956
7588
  this.attachVisibilityListener();
6957
7589
  this.connect();
7590
+ profile.startRealtime = duration(realtimeStartedAt);
7591
+ profile.controllerTotal = duration(renderStartedAt);
6958
7592
  return {
6959
7593
  doc: res.doc,
6960
7594
  salesClosed: !!res.event.salesClosed,
@@ -6965,7 +7599,8 @@ var PickerController = class {
6965
7599
  currency,
6966
7600
  // 'test' ⇒ sandbox event: hosts render a TEST MODE ribbon (Phase 11).
6967
7601
  mode: res.event.mode ?? "live",
6968
- posterUrl: res.event.posterUrl ?? null
7602
+ posterUrl: res.event.posterUrl ?? null,
7603
+ performanceProfile: profile
6969
7604
  };
6970
7605
  }
6971
7606
  // ---- selection ------------------------------------------------------------
@@ -7021,11 +7656,23 @@ var PickerController = class {
7021
7656
  select(ids) {
7022
7657
  const before = new Set(this.getSelection().map((seat) => seat.label));
7023
7658
  const expanded = /* @__PURE__ */ new Set();
7659
+ const rejected = [];
7024
7660
  for (const id of ids) {
7025
7661
  const table = this.groupedTableBySeatId.get(id) ?? this.groupedTablesByLabel.get(id);
7026
- if (table) table.chairs.forEach((chair) => expanded.add(chair.id));
7027
- else expanded.add(id);
7662
+ if (table) {
7663
+ const resolution = this.tableTierResolution(table);
7664
+ if (resolution.kind === "invalid") rejected.push(table.label);
7665
+ else table.chairs.forEach((chair) => expanded.add(chair.id));
7666
+ } else {
7667
+ const seat = this.seatById.get(id);
7668
+ if (seat && this.seatTierResolution(seat).kind === "invalid") rejected.push(seat.label);
7669
+ else expanded.add(id);
7670
+ }
7671
+ }
7672
+ if (rejected.length) {
7673
+ this.emitError(new PickerSelectionError(ticketOptionNotApplicableIssue(rejected)));
7028
7674
  }
7675
+ if (!expanded.size) return [];
7029
7676
  this.renderer?.select?.([...expanded]);
7030
7677
  if (this.selectionGuestCount() > this.maxSelection) {
7031
7678
  this.renderer?.deselect([...expanded]);
@@ -7093,6 +7740,13 @@ var PickerController = class {
7093
7740
  }
7094
7741
  handleRendererSelect(seat) {
7095
7742
  const table = this.groupedTableBySeatId.get(seat.id);
7743
+ const resolution = table ? this.tableTierResolution(table) : this.seatTierResolution(seat);
7744
+ if (resolution.kind === "invalid") {
7745
+ this.renderer?.deselect(table ? table.chairs.map((chair) => chair.id) : [seat.id]);
7746
+ this.emitError(new PickerSelectionError(ticketOptionNotApplicableIssue([table?.label ?? seat.label])));
7747
+ this.emitSelectionChange();
7748
+ return;
7749
+ }
7096
7750
  if (table) {
7097
7751
  if (this.selectionGuestCount() > this.maxSelection) {
7098
7752
  this.renderer?.deselect(table.chairs.map((chair) => chair.id));
@@ -7174,6 +7828,8 @@ var PickerController = class {
7174
7828
  const existingGA = (this.hold_?.items ?? []).filter((item) => item.objectType === "ga");
7175
7829
  const combinedLabels = [.../* @__PURE__ */ new Set([...existingGA.map((item) => item.label), ...labels])];
7176
7830
  if (!combinedLabels.length) return null;
7831
+ const restriction = this.ticketIssue(combinedLabels);
7832
+ if (restriction) throw new PickerSelectionError(restriction);
7177
7833
  if (this.hold_ && this.holdCovers(combinedLabels)) return this.hold_;
7178
7834
  try {
7179
7835
  const selections = combinedLabels.map((label) => {
@@ -7345,6 +8001,7 @@ var PickerController = class {
7345
8001
  const available = /* @__PURE__ */ new Set();
7346
8002
  for (const seat of seats) {
7347
8003
  if (held.has(seat.label)) continue;
8004
+ if (this.seatTierResolution(seat).kind === "invalid") continue;
7348
8005
  const id = this.labelToId.get(seat.label);
7349
8006
  const status = id ? this.getStatus(id) : void 0;
7350
8007
  if ((status ?? "free") === "free") available.add(seat.label);
@@ -7426,6 +8083,8 @@ var PickerController = class {
7426
8083
  if (!this.api.book) throw new Error("picker: transport has no book() \u2014 hold-only mode");
7427
8084
  const labels = labelsArg ?? this.getSelection().map((seat) => seat.label);
7428
8085
  if (!labels.length) return null;
8086
+ const restriction = this.ticketIssue(labels);
8087
+ if (restriction) throw new PickerSelectionError(restriction);
7429
8088
  let holdId;
7430
8089
  try {
7431
8090
  if (this.hold_ && this.holdCovers(labels)) {
@@ -7552,6 +8211,20 @@ var PickerController = class {
7552
8211
  getStatus(id) {
7553
8212
  return this.renderer?.getStatus(id);
7554
8213
  }
8214
+ /** Seat ids whose category the author marked not-for-sale. */
8215
+ notForSaleCategorySeatIds() {
8216
+ const doc = this.doc;
8217
+ if (!doc) return [];
8218
+ const flagged = new Set(
8219
+ doc.categories.filter((category) => category.notForSale).map((category) => category.key)
8220
+ );
8221
+ if (!flagged.size) return [];
8222
+ const ids = [];
8223
+ for (const [id, seat] of this.seatById) {
8224
+ if (flagged.has(seat.categoryKey)) ids.push(id);
8225
+ }
8226
+ return ids;
8227
+ }
7555
8228
  flashSeat(id, color) {
7556
8229
  this.renderer?.flashSeat(id, color);
7557
8230
  }
@@ -7564,6 +8237,11 @@ var PickerController = class {
7564
8237
  zoomToFit() {
7565
8238
  this.renderer?.zoomToFit();
7566
8239
  }
8240
+ /** Preserve section focus while the usable map viewport changes. */
8241
+ refitCurrentView() {
8242
+ if (this.renderer?.refitCurrentView) this.renderer.refitCurrentView();
8243
+ else this.renderer?.zoomToFit();
8244
+ }
7567
8245
  worldToScreen(p) {
7568
8246
  return this.renderer?.worldToScreen(p) ?? { x: 0, y: 0 };
7569
8247
  }
@@ -7593,7 +8271,8 @@ var PickerController = class {
7593
8271
  /** Switch the shown floor (2D), then re-apply live seat statuses onto it. */
7594
8272
  setFloor(id) {
7595
8273
  const r = this.renderer;
7596
- if (!r?.setActiveFloor || id === r.getActiveFloorId?.()) return;
8274
+ if (!r?.setActiveFloor || id === r.getActiveFloorId?.() && !r.isFloorOverview?.()) return;
8275
+ r.setFloorOverview?.(false);
7597
8276
  r.setStacked?.(false);
7598
8277
  r.setActiveFloor(id);
7599
8278
  void this.resnapshot();
@@ -7609,12 +8288,24 @@ var PickerController = class {
7609
8288
  isMultiFloor() {
7610
8289
  return (this._doc?.floors?.length ?? 0) > 1;
7611
8290
  }
8291
+ /** Enter/leave the source-coordinate all-floor buyer overview. */
8292
+ setFloorOverview(on) {
8293
+ const r = this.renderer;
8294
+ if (!r?.setFloorOverview || on === r.isFloorOverview?.()) return;
8295
+ r.setFloorOverview(on);
8296
+ void this.resnapshot();
8297
+ this.emitSelectionChange();
8298
+ }
8299
+ isFloorOverview() {
8300
+ return this.renderer?.isFloorOverview?.() ?? false;
8301
+ }
7612
8302
  /** Tap-a-deck-to-enter: leave the 3D stack, drop to flat 2D on `floorId`, and
7613
8303
  * tell the host so it can sync its 2D/3D toggle + floor-switcher state. */
7614
8304
  handleDeckTap(floorId) {
7615
8305
  const r = this.renderer;
7616
8306
  if (!r) return;
7617
8307
  r.setViewMode?.("flat");
8308
+ r.setFloorOverview?.(false);
7618
8309
  r.setStacked?.(false);
7619
8310
  r.setActiveFloor?.(floorId);
7620
8311
  void this.resnapshot();
@@ -7683,10 +8374,30 @@ var PickerController = class {
7683
8374
  if (!r?.getVisibleWorldRect || !r.getWorldBounds) return null;
7684
8375
  return { visible: r.getVisibleWorldRect(), bounds: r.getWorldBounds() };
7685
8376
  }
8377
+ getMinimapSnapshot() {
8378
+ return this.renderer?.getMinimapSnapshot?.() ?? { sections: [], seats: [] };
8379
+ }
8380
+ panToWorld(point) {
8381
+ this.renderer?.panToWorld?.(point);
8382
+ }
8383
+ /** Leave section-focus dimming without changing the current zoom. Used when
8384
+ * the minimap viewport itself is dragged to a different part of the venue. */
8385
+ clearSectionFocus() {
8386
+ this.renderer?.clearSectionFocus?.();
8387
+ this.opts.onSectionFocus?.(null);
8388
+ }
8389
+ /** Read-only browser-render evidence for parity/performance audit tooling. */
8390
+ getRenderedQualityEvidence() {
8391
+ return this.renderer?.getRenderedQualityEvidence() ?? null;
8392
+ }
7686
8393
  /** Glide in on a section and surface its summary (same path as a section tap). */
7687
8394
  focusSection(id) {
7688
8395
  this.handleSectionTap(id);
7689
8396
  }
8397
+ /** Live seat membership count used by buyer section-navigation controls. */
8398
+ getSectionSeatCount(id) {
8399
+ return this.renderer?.sectionMembers?.(id)?.length ?? 0;
8400
+ }
7690
8401
  /** Zoom back out to the whole chart and clear the section-summary card + focus. */
7691
8402
  overview() {
7692
8403
  this.renderer?.clearSectionFocus?.();
@@ -7739,15 +8450,21 @@ var PickerController = class {
7739
8450
  }
7740
8451
  const categories = [...byCat.entries()].map(([key, count]) => {
7741
8452
  const cat = doc.categories.find((c) => c.key === key);
8453
+ const prices = cat?.tiers?.length ? cat.tiers.map((tier) => tier.price) : [cat?.price ?? 0];
8454
+ const priceMin = Math.min(...prices);
8455
+ const priceMax = Math.max(...prices);
7742
8456
  return {
7743
8457
  key,
7744
8458
  count,
7745
8459
  label: cat?.label ?? key,
7746
8460
  color: cat?.color ?? "#6e7bff",
7747
- price: cat?.price ?? 0
8461
+ price: priceMin,
8462
+ priceMin,
8463
+ priceMax
7748
8464
  };
7749
8465
  }).sort((a, b) => a.price - b.price);
7750
- const prices = categories.map((c) => c.price);
8466
+ const minimums = categories.map((c) => c.priceMin);
8467
+ const maximums = categories.map((c) => c.priceMax);
7751
8468
  const zone = sec.zone ? doc.zones?.find((z) => z.id === sec.zone) : void 0;
7752
8469
  const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
7753
8470
  return {
@@ -7757,8 +8474,8 @@ var PickerController = class {
7757
8474
  ...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
7758
8475
  color,
7759
8476
  seatsLeft,
7760
- priceMin: prices.length ? prices[0] : 0,
7761
- priceMax: prices.length ? prices[prices.length - 1] : 0,
8477
+ priceMin: minimums.length ? Math.min(...minimums) : 0,
8478
+ priceMax: maximums.length ? Math.max(...maximums) : 0,
7762
8479
  categories
7763
8480
  };
7764
8481
  }
@@ -7799,11 +8516,12 @@ var PickerController = class {
7799
8516
  const accessibility = s.accessibility?.length ? { accessibility: s.accessibility } : void 0;
7800
8517
  const wheelchair = s.wheelchairSpaceType ? { wheelchairSpaceType: s.wheelchairSpaceType } : void 0;
7801
8518
  const context = this.seatContext.get(s.id);
7802
- const tiers = this.tiersFor(s.categoryKey);
7803
- if (!tiers) {
7804
- return { id: s.id, label: s.label, ...display, ...context, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial, ...accessibility, ...wheelchair };
8519
+ const tiers = this.tiersForSeat(s);
8520
+ const resolution = this.seatTierResolution(s);
8521
+ if (resolution.kind !== "resolved" || !tiers) {
8522
+ return { id: s.id, label: s.label, ...display, ...context, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), currency: this.currency, ...commercial, ...accessibility, ...wheelchair };
7805
8523
  }
7806
- const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
8524
+ const chosen = resolution.tier;
7807
8525
  return {
7808
8526
  id: s.id,
7809
8527
  label: s.label,
@@ -7811,6 +8529,7 @@ var PickerController = class {
7811
8529
  ...context,
7812
8530
  categoryKey: s.categoryKey,
7813
8531
  price: chosen.price,
8532
+ currency: this.currency,
7814
8533
  tiers,
7815
8534
  tierId: chosen.id,
7816
8535
  ...commercial,
@@ -7820,8 +8539,9 @@ var PickerController = class {
7820
8539
  }
7821
8540
  toTableSeat(table) {
7822
8541
  const representative = table.chairs[0];
7823
- const tiers = this.tiersFor(table.categoryKey);
7824
- const chosen = tiers?.find((tier) => tier.id === this.seatTiers.get(representative?.id ?? "")) ?? tiers?.[0];
8542
+ const tiers = this.tiersFor(table.categoryKey)?.filter((tier) => resolveTicketTierForSeat(this._doc, this.tableTierSeat(table), tier.id).kind === "resolved");
8543
+ const resolution = this.tableTierResolution(table);
8544
+ const chosen = resolution.kind === "resolved" ? resolution.tier : void 0;
7825
8545
  return {
7826
8546
  id: representative?.id ?? table.objectId,
7827
8547
  label: table.label,
@@ -7832,6 +8552,7 @@ var PickerController = class {
7832
8552
  rowLabel: table.displayLabel ?? table.label,
7833
8553
  categoryKey: table.categoryKey,
7834
8554
  price: chosen?.price ?? this.priceFor(table.categoryKey),
8555
+ currency: this.currency,
7835
8556
  ...tiers ? { tiers, tierId: chosen?.id } : {},
7836
8557
  objectType: "table",
7837
8558
  bookingMode: table.mode,
@@ -7861,6 +8582,27 @@ var PickerController = class {
7861
8582
  const t2 = this._doc?.categories.find((c) => c.key === categoryKey)?.tiers;
7862
8583
  return t2 && t2.length ? t2 : void 0;
7863
8584
  }
8585
+ tiersForSeat(seat) {
8586
+ if (!this._doc) return void 0;
8587
+ const tiers = applicableTicketTiers(this._doc, seat);
8588
+ return tiers.length ? tiers : void 0;
8589
+ }
8590
+ seatTierResolution(seat) {
8591
+ if (!this._doc) return { kind: "none" };
8592
+ return resolveTicketTierForSeat(this._doc, seat, this.seatTiers.get(seat.id));
8593
+ }
8594
+ tableTierSeat(table) {
8595
+ return {
8596
+ label: table.label,
8597
+ categoryKey: table.categoryKey,
8598
+ accessibility: table.chairs.flatMap((chair) => chair.accessibility ?? [])
8599
+ };
8600
+ }
8601
+ tableTierResolution(table) {
8602
+ if (!this._doc) return { kind: "none" };
8603
+ const representativeId = table.chairs[0]?.id ?? "";
8604
+ return resolveTicketTierForSeat(this._doc, this.tableTierSeat(table), this.seatTiers.get(representativeId));
8605
+ }
7864
8606
  /**
7865
8607
  * Choose a ticket tier for a selected seat (e.g. Adult → Child). Re-emits the
7866
8608
  * selection so the host's cart + the eventual hold carry the new tier + price.
@@ -7870,7 +8612,7 @@ var PickerController = class {
7870
8612
  setSeatTier(seatId, tierId) {
7871
8613
  const seat = this.seatById.get(seatId);
7872
8614
  if (!seat) return;
7873
- const tiers = this.tiersFor(seat.categoryKey);
8615
+ const tiers = this.tiersForSeat(seat);
7874
8616
  if (!tiers) return;
7875
8617
  if (tierId == null) this.seatTiers.delete(seatId);
7876
8618
  else if (tiers.some((t2) => t2.id === tierId)) this.seatTiers.set(seatId, tierId);
@@ -7891,6 +8633,16 @@ var PickerController = class {
7891
8633
  this.opts.onSelectionChange?.(this.getSelection());
7892
8634
  this.emitOrphanHint();
7893
8635
  }
8636
+ /** Check explicit tier applicability and companion relationships for the
8637
+ * current (or supplied) seats. Ticket display names are never semantic input. */
8638
+ ticketIssue(labels) {
8639
+ if (!this._doc) return null;
8640
+ const seats = labels ? this.seatsForLabels(labels) : this.getSelection();
8641
+ return ticketSelectionIssue(this._doc, seats.map((seat) => ({
8642
+ label: seat.label,
8643
+ ...seat.tierId ? { tierId: seat.tierId } : {}
8644
+ })));
8645
+ }
7894
8646
  /**
7895
8647
  * Orphan-seat advice on manual selection: when the buyer's current picks
7896
8648
  * strand one (or more) single free seats between unavailable same-row
@@ -7901,6 +8653,12 @@ var PickerController = class {
7901
8653
  if (!this.opts.onHint) return;
7902
8654
  const r = this.renderer;
7903
8655
  if (!r) return;
8656
+ const restriction = this.ticketIssue();
8657
+ if (restriction) {
8658
+ this.hintShown = true;
8659
+ this.opts.onHint(restriction.code === "companion_pair_required" ? t("picker.companionPairRequired") : "That ticket choice is not available for the selected seat.");
8660
+ return;
8661
+ }
7904
8662
  const selected = new Set(r.getSelection().map((s) => s.id));
7905
8663
  const stranded = selected.size ? strandedSingles(this.seatById.values(), (id) => r.getStatus(id), selected) : [];
7906
8664
  if (stranded.length > 0) {
@@ -8022,6 +8780,8 @@ var PickerController = class {
8022
8780
  ["free", "held", "booked", "not_for_sale"].forEach((st) => {
8023
8781
  if (st !== defaultSeatStatus && byStatus[st].length) r.setStatus(byStatus[st], st);
8024
8782
  });
8783
+ const notForSaleCategoryIds = this.notForSaleCategorySeatIds();
8784
+ if (notForSaleCategoryIds.length) r.setStatus(notForSaleCategoryIds, "not_for_sale");
8025
8785
  this.clearBookedHoldIfSettled();
8026
8786
  this.opts.onStatusChange?.();
8027
8787
  }
@@ -9374,9 +10134,9 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
9374
10134
 
9375
10135
  // src/i18n/bundles.ts
9376
10136
  var LOADERS = {
9377
- es: () => import("./es-WEC5NHRT.js").then((m) => ({ default: m.es })),
9378
- de: () => import("./de-ST4H423D.js").then((m) => ({ default: m.de })),
9379
- fr: () => import("./fr-SGBFNAQV.js").then((m) => ({ default: m.fr }))
10137
+ es: () => import("./es-VMBTGVFQ.js").then((m) => ({ default: m.es })),
10138
+ de: () => import("./de-YGODYJ3E.js").then((m) => ({ default: m.de })),
10139
+ fr: () => import("./fr-SLBE2ULB.js").then((m) => ({ default: m.fr }))
9380
10140
  };
9381
10141
  var loaded = /* @__PURE__ */ new Set(["en"]);
9382
10142
  async function loadLocale(code) {
@@ -9406,6 +10166,7 @@ export {
9406
10166
  MAX_EVENT_INVENTORY,
9407
10167
  MAX_GA_CAPACITY,
9408
10168
  PickerController,
10169
+ PickerSelectionError,
9409
10170
  RENDERED_QUALITY_REPORT_VERSION,
9410
10171
  SEAT_COMMERCIAL_MARKS,
9411
10172
  SUPPORTED_LOCALES,
@@ -9429,6 +10190,7 @@ export {
9429
10190
  expandTable,
9430
10191
  expandTableSlots,
9431
10192
  floorObjects,
10193
+ floorOverviewTranslations,
9432
10194
  floorsOf,
9433
10195
  formatDate,
9434
10196
  formatMinor,
@@ -9447,6 +10209,7 @@ export {
9447
10209
  isGaUnitLabel,
9448
10210
  isSectionHidden,
9449
10211
  layerOf,
10212
+ layoutFloorOverview,
9450
10213
  loadLocale,
9451
10214
  objectCenter,
9452
10215
  owningSectionForObject,
@@ -9466,6 +10229,7 @@ export {
9466
10229
  tCount,
9467
10230
  tableInventoryCount,
9468
10231
  tableSeatCountsBySide,
10232
+ translateObject,
9469
10233
  validGAInventorySegments
9470
10234
  };
9471
10235
  //# sourceMappingURL=index.js.map