@seatlayer/core 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -41,29 +41,17 @@ var init_es = __esm({
41
41
  "common.done": "Listo",
42
42
  "common.copied": "\u2713 Copiado",
43
43
  // buyer picker (extraction in progress — P4 of the port plan)
44
- "picker.holdSeats": "Retener asientos y comprar",
45
- "picker.completeBooking": "Completar reserva",
46
- "picker.seatsHeld": "Asientos retenidos \u2014 {time}",
47
44
  "picker.holdExpired": "Tu retenci\xF3n expir\xF3 \u2014 los asientos fueron liberados. Elige de nuevo.",
48
- "picker.seatTaken": "El asiento {label} fue tomado por otro comprador.",
49
45
  "picker.poweredBy": "Con la tecnolog\xEDa de SeatLayer",
50
46
  "picker.testMode": "MODO DE PRUEBA",
51
- "picker.colorblind": "Colores aptos para daltonismo",
52
47
  "picker.orphanHint": "Esto deja un asiento aislado \u2014 considera desplazarte un asiento.",
53
- "picker.seats.one": "{count} asiento",
54
- "picker.seats.other": "{count} asientos",
55
48
  // renderer (drawn on the Konva map — shared by the embed SDK)
56
49
  "map.aria": "Mapa de asientos. Usa las flechas para moverte entre asientos, Intro para seleccionar.",
57
50
  "map.seatsLeft": "{count} LIBRES",
58
51
  "map.statusHeld": "En espera",
59
52
  "map.statusTaken": "Ocupado",
60
53
  "map.fromPrice": "DESDE {price}",
61
- // buyer picker page (src/pages/PickerPage.tsx)
62
- "picker.language": "Idioma",
63
- "picker.zoomToFit": "Ajustar zoom",
64
- "picker.seatCountLabel": "asientos",
65
- "picker.capacity": "capacidad",
66
- "picker.viewMode": "Modo de vista",
54
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
67
55
  "picker.floor": "Piso",
68
56
  "picker.zoomLevel": "Nivel de zoom",
69
57
  "picker.rungTip.zones": "Vista general \u2014 grupos de secciones como Grada norte o VIP",
@@ -78,37 +66,19 @@ var init_es = __esm({
78
66
  "picker.seatsLeftInSection.other": "{count} asientos disponibles",
79
67
  "picker.overview": "Descripci\xF3n general",
80
68
  "picker.tapSeatHint": "Toca cualquier asiento para ver su vista",
81
- "picker.chartSize": "Tama\xF1o del gr\xE1fico",
82
- "picker.custom": "Personalizado",
83
- "picker.categories": "Categor\xEDas",
84
- "picker.accessibility": "Accesibilidad",
85
- "picker.showAnyAccessible": "Mostrar cualquier asiento accesible",
86
- "picker.any": "Cualquiera",
87
- "picker.yourSeats": "Tus asientos",
88
- "picker.emptySeats": "Toca asientos en el mapa",
89
- "picker.emptySeatsWithGa": "Toca asientos en el mapa \xB7 toca un \xE1rea de pie para entradas de entrada general",
90
- "picker.oneFewer": "Uno menos",
91
- "picker.oneMore": "Uno m\xE1s",
92
- "picker.remove": "Eliminar {label}",
93
69
  "picker.ticketTierFor": "Categor\xEDa de entrada para {label}",
94
- "picker.total": "Total: {amount}",
95
70
  "picker.viewFromSeat": "Vista desde el asiento {label}",
96
71
  "picker.real360": "REAL 360\xB0",
97
72
  "picker.preview": "VISTA PREVIA",
98
- "picker.open360": "Abrir vista de 360\xB0",
99
73
  "picker.sightline": "\u2248 {m} m al escenario \xB7 visi\xF3n despejada",
100
- "picker.bookedDemo": "\xA1Reservado! (demostraci\xF3n)",
101
- "picker.bookButton.one": "Reservar {count} entrada \u2014 {amount}",
102
- "picker.bookButton.other": "Reservar {count} entradas \u2014 {amount}",
103
- "picker.simulateCrowd": "Simular multitud: {state}",
104
- "picker.on": "ACTIVADO",
105
- "picker.off": "DESACTIVADO",
106
74
  "picker.panorama360": "Foto de 360\xB0 del lugar",
107
75
  "picker.illustrationCaption": "ilustraci\xF3n \xB7 \u2248 {m} m del escenario",
108
76
  "picker.restrictedView": "Visibilidad restringida",
109
77
  "picker.obstructedView": "Visibilidad obstruida",
110
78
  "picker.premiumSeat": "Asiento premium",
111
- "picker.hideLimitedView": "Ocultar asientos con visibilidad limitada"
79
+ "picker.hideLimitedView": "Ocultar asientos con visibilidad limitada",
80
+ "picker.bestSeatsPremium": "Mejores asientos",
81
+ "picker.premiumFallbackNote": "No hay un bloque premium de {count}: mostrando los mejores disponibles"
112
82
  };
113
83
  }
114
84
  });
@@ -129,29 +99,17 @@ var init_de = __esm({
129
99
  "common.done": "Fertig",
130
100
  "common.copied": "\u2713 Kopiert",
131
101
  // buyer picker (extraction in progress — P4 of the port plan)
132
- "picker.holdSeats": "Pl\xE4tze reservieren & Kasse",
133
- "picker.completeBooking": "Buchung abschlie\xDFen",
134
- "picker.seatsHeld": "Pl\xE4tze reserviert \u2014 {time}",
135
102
  "picker.holdExpired": "Ihre Reservierung ist abgelaufen \u2014 die Pl\xE4tze wurden freigegeben. Bitte erneut ausw\xE4hlen.",
136
- "picker.seatTaken": "Platz {label} wurde gerade von einem anderen K\xE4ufer gebucht.",
137
103
  "picker.poweredBy": "Bereitgestellt von SeatLayer",
138
104
  "picker.testMode": "TESTMODUS",
139
- "picker.colorblind": "Farbenblind-freundliche Farben",
140
105
  "picker.orphanHint": "Dadurch bleibt ein einzelner Platz \xFCbrig \u2014 r\xFCcken Sie ggf. einen Platz weiter.",
141
- "picker.seats.one": "{count} Platz",
142
- "picker.seats.other": "{count} Pl\xE4tze",
143
106
  // renderer (drawn on the Konva map — shared by the embed SDK)
144
107
  "map.aria": "Sitzplan. Mit Pfeiltasten zwischen Pl\xE4tzen navigieren, Eingabe zum Ausw\xE4hlen.",
145
108
  "map.seatsLeft": "{count} FREI",
146
109
  "map.statusHeld": "Reserviert",
147
110
  "map.statusTaken": "Vergeben",
148
111
  "map.fromPrice": "AB {price}",
149
- // buyer picker page (src/pages/PickerPage.tsx)
150
- "picker.language": "Sprache",
151
- "picker.zoomToFit": "Auf Gr\xF6\xDFe anpassen",
152
- "picker.seatCountLabel": "Pl\xE4tze",
153
- "picker.capacity": "Kapazit\xE4t",
154
- "picker.viewMode": "Ansichtsmodus",
112
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
155
113
  "picker.floor": "Etage",
156
114
  "picker.zoomLevel": "Zoomstufe",
157
115
  "picker.rungTip.zones": "\xDCbersicht \u2014 Bereichsgruppen wie Nordtrib\xFCne oder VIP",
@@ -166,37 +124,19 @@ var init_de = __esm({
166
124
  "picker.seatsLeftInSection.other": "{count} Pl\xE4tze verf\xFCgbar",
167
125
  "picker.overview": "\xDCbersicht",
168
126
  "picker.tapSeatHint": "Tippen Sie auf einen Platz, um die Ansicht zu pr\xFCfen",
169
- "picker.chartSize": "Kartengr\xF6\xDFe",
170
- "picker.custom": "Benutzerdefiniert",
171
- "picker.categories": "Kategorien",
172
- "picker.accessibility": "Barrierefreiheit",
173
- "picker.showAnyAccessible": "Jeden barrierefreien Platz anzeigen",
174
- "picker.any": "Beliebig",
175
- "picker.yourSeats": "Ihre Pl\xE4tze",
176
- "picker.emptySeats": "Tippen Sie auf die Pl\xE4tze auf der Karte",
177
- "picker.emptySeatsWithGa": "Tippen Sie auf die Pl\xE4tze auf der Karte \xB7 tippen Sie auf einen Stehbereich f\xFCr Stehplatztickets",
178
- "picker.oneFewer": "Einen weniger",
179
- "picker.oneMore": "Einen mehr",
180
- "picker.remove": "{label} entfernen",
181
127
  "picker.ticketTierFor": "Ticketklasse f\xFCr {label}",
182
- "picker.total": "Gesamt: {amount}",
183
128
  "picker.viewFromSeat": "Ansicht von Platz {label}",
184
129
  "picker.real360": "REAL 360\xB0",
185
130
  "picker.preview": "VORSCHAU",
186
- "picker.open360": "\xD6ffnen Sie die 360\xB0-Ansicht",
187
131
  "picker.sightline": "\u2248 {m} m zur B\xFChne \xB7 freie Sicht",
188
- "picker.bookedDemo": "Gebucht! (Demo)",
189
- "picker.bookButton.one": "{count} Ticket buchen \u2014 {amount}",
190
- "picker.bookButton.other": "{count} Tickets buchen \u2014 {amount}",
191
- "picker.simulateCrowd": "Menschenmenge simulieren: {state}",
192
- "picker.on": "AN",
193
- "picker.off": "AUS",
194
132
  "picker.panorama360": "360\xB0-Veranstaltungsfotos",
195
133
  "picker.illustrationCaption": "Illustration \xB7 \u2248 {m} m von der B\xFChne",
196
134
  "picker.restrictedView": "Eingeschr\xE4nkte Sicht",
197
135
  "picker.obstructedView": "Sichtbehinderung",
198
136
  "picker.premiumSeat": "Premium-Platz",
199
- "picker.hideLimitedView": "Pl\xE4tze mit eingeschr\xE4nkter Sicht ausblenden"
137
+ "picker.hideLimitedView": "Pl\xE4tze mit eingeschr\xE4nkter Sicht ausblenden",
138
+ "picker.bestSeatsPremium": "Beste Pl\xE4tze",
139
+ "picker.premiumFallbackNote": "Kein Premium-Block mit {count} Pl\xE4tzen \u2014 beste verf\xFCgbare werden angezeigt"
200
140
  };
201
141
  }
202
142
  });
@@ -217,29 +157,17 @@ var init_fr = __esm({
217
157
  "common.done": "Termin\xE9",
218
158
  "common.copied": "\u2713 Copi\xE9",
219
159
  // buyer picker (extraction in progress — P4 of the port plan)
220
- "picker.holdSeats": "R\xE9server les si\xE8ges et valider",
221
- "picker.completeBooking": "Terminer la r\xE9servation",
222
- "picker.seatsHeld": "Si\xE8ges r\xE9serv\xE9s \u2014 {time}",
223
160
  "picker.holdExpired": "Votre r\xE9servation a expir\xE9 \u2014 les si\xE8ges ont \xE9t\xE9 lib\xE9r\xE9s. S\xE9lectionnez \xE0 nouveau.",
224
- "picker.seatTaken": "Le si\xE8ge {label} vient d'\xEAtre r\xE9serv\xE9 par un autre acheteur.",
225
161
  "picker.poweredBy": "Propuls\xE9 par SeatLayer",
226
162
  "picker.testMode": "MODE TEST",
227
- "picker.colorblind": "Couleurs adapt\xE9es au daltonisme",
228
163
  "picker.orphanHint": "Cela laisse un si\xE8ge isol\xE9 \u2014 pensez \xE0 vous d\xE9caler d'un si\xE8ge.",
229
- "picker.seats.one": "{count} si\xE8ge",
230
- "picker.seats.other": "{count} si\xE8ges",
231
164
  // renderer (drawn on the Konva map — shared by the embed SDK)
232
165
  "map.aria": "Plan de salle. Utilisez les fl\xE8ches pour naviguer entre les si\xE8ges, Entr\xE9e pour s\xE9lectionner.",
233
166
  "map.seatsLeft": "{count} LIBRES",
234
167
  "map.statusHeld": "En attente",
235
168
  "map.statusTaken": "Pris",
236
169
  "map.fromPrice": "D\xC8S {price}",
237
- // buyer picker page (src/pages/PickerPage.tsx)
238
- "picker.language": "Langue",
239
- "picker.zoomToFit": "Ajuster le zoom",
240
- "picker.seatCountLabel": "si\xE8ges",
241
- "picker.capacity": "capacit\xE9",
242
- "picker.viewMode": "Mode d'affichage",
170
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
243
171
  "picker.floor": "\xC9tage",
244
172
  "picker.zoomLevel": "Niveau de zoom",
245
173
  "picker.rungTip.zones": "Vue g\xE9n\xE9rale \u2014 groupes de sections comme Tribune nord ou VIP",
@@ -254,37 +182,19 @@ var init_fr = __esm({
254
182
  "picker.seatsLeftInSection.other": "{count} si\xE8ges restants",
255
183
  "picker.overview": "Vue d'ensemble",
256
184
  "picker.tapSeatHint": "Appuyez sur un si\xE8ge pour v\xE9rifier sa vue",
257
- "picker.chartSize": "Taille du plan",
258
- "picker.custom": "Personnalis\xE9",
259
- "picker.categories": "Cat\xE9gories",
260
- "picker.accessibility": "Accessibilit\xE9",
261
- "picker.showAnyAccessible": "Afficher un si\xE8ge accessible",
262
- "picker.any": "N'importe quel",
263
- "picker.yourSeats": "Vos si\xE8ges",
264
- "picker.emptySeats": "Appuyez sur les si\xE8ges sur le plan",
265
- "picker.emptySeatsWithGa": "Appuyez sur les si\xE8ges sur le plan \xB7 appuyez sur une zone debout pour les billets GA",
266
- "picker.oneFewer": "Un de moins",
267
- "picker.oneMore": "Un de plus",
268
- "picker.remove": "Supprimer {label}",
269
185
  "picker.ticketTierFor": "Cat\xE9gorie de billet pour {label}",
270
- "picker.total": "Total : {amount}",
271
186
  "picker.viewFromSeat": "Vue depuis le si\xE8ge {label}",
272
187
  "picker.real360": "VRAI 360\xB0",
273
188
  "picker.preview": "APER\xC7U",
274
- "picker.open360": "Ouvrir la vue \xE0 360\xB0",
275
189
  "picker.sightline": "\u2248 {m} m de la sc\xE8ne \xB7 vue d\xE9gag\xE9e",
276
- "picker.bookedDemo": "R\xE9serv\xE9 ! (d\xE9mo)",
277
- "picker.bookButton.one": "R\xE9server {count} billet \u2014 {amount}",
278
- "picker.bookButton.other": "R\xE9server {count} billets \u2014 {amount}",
279
- "picker.simulateCrowd": "Simuler la foule : {state}",
280
- "picker.on": "ACTIV\xC9",
281
- "picker.off": "D\xC9SACTIV\xC9",
282
190
  "picker.panorama360": "Photo panoramique 360\xB0 du lieu",
283
191
  "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m de la sc\xE8ne",
284
192
  "picker.restrictedView": "Visibilit\xE9 r\xE9duite",
285
193
  "picker.obstructedView": "Vue obstru\xE9e",
286
194
  "picker.premiumSeat": "Place premium",
287
- "picker.hideLimitedView": "Masquer les places \xE0 visibilit\xE9 r\xE9duite"
195
+ "picker.hideLimitedView": "Masquer les places \xE0 visibilit\xE9 r\xE9duite",
196
+ "picker.bestSeatsPremium": "Meilleures places",
197
+ "picker.premiumFallbackNote": "Aucun bloc premium de {count} \u2014 affichage des meilleures places disponibles"
288
198
  };
289
199
  }
290
200
  });
@@ -337,6 +247,8 @@ __export(index_exports, {
337
247
  pointInPolygonWithHoles: () => pointInPolygonWithHoles,
338
248
  polygonLabelPoint: () => polygonLabelPoint,
339
249
  resolveLocale: () => resolveLocale,
250
+ rowSeatPositions: () => rowSeatPositions,
251
+ seatLabelPart: () => seatLabelPart,
340
252
  setLocale: () => setLocale,
341
253
  setMoneyLocale: () => setMoneyLocale,
342
254
  setStringOverrides: () => setStringOverrides,
@@ -459,6 +371,53 @@ function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected
459
371
  };
460
372
  }
461
373
 
374
+ // src/core/labeling.ts
375
+ var FULL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
376
+ var LOWER_ALPHABET = "abcdefghijklmnopqrstuvwxyz";
377
+ function toBijectiveBase(index, alphabet) {
378
+ const base = alphabet.length;
379
+ let n = index + 1;
380
+ let out = "";
381
+ while (n > 0) {
382
+ n -= 1;
383
+ const rem = n % base;
384
+ out = alphabet[rem] + out;
385
+ n = Math.floor(n / base);
386
+ }
387
+ return out;
388
+ }
389
+ function toLetters(value, lower = false) {
390
+ const alphabet = lower ? LOWER_ALPHABET : FULL_ALPHABET;
391
+ return toBijectiveBase(Math.max(0, Math.floor(value) - 1), alphabet);
392
+ }
393
+ var ROMAN_TABLE = [
394
+ [1e3, "M"],
395
+ [900, "CM"],
396
+ [500, "D"],
397
+ [400, "CD"],
398
+ [100, "C"],
399
+ [90, "XC"],
400
+ [50, "L"],
401
+ [40, "XL"],
402
+ [10, "X"],
403
+ [9, "IX"],
404
+ [5, "V"],
405
+ [4, "IV"],
406
+ [1, "I"]
407
+ ];
408
+ function toRoman(value) {
409
+ if (!Number.isFinite(value) || value <= 0) return String(value);
410
+ let remaining = Math.floor(value);
411
+ let out = "";
412
+ for (const [n, sym] of ROMAN_TABLE) {
413
+ while (remaining >= n) {
414
+ out += sym;
415
+ remaining -= n;
416
+ }
417
+ }
418
+ return out;
419
+ }
420
+
462
421
  // src/core/layout.ts
463
422
  function overrideAccessibility(o) {
464
423
  if (!o) return [];
@@ -504,24 +463,62 @@ function overrideMap(row) {
504
463
  if (row.overrides) for (const o of row.overrides) m.set(o.index, o);
505
464
  return m;
506
465
  }
507
- function expandRowSlots(row) {
508
- const start = row.seatLabelStart ?? 1;
466
+ function centerRank(n) {
467
+ const rank = new Array(n);
468
+ Array.from({ length: n }, (_, i) => i).sort((a, b) => Math.abs(2 * a - (n - 1)) - Math.abs(2 * b - (n - 1)) || a - b).forEach((idx, k) => rank[idx] = k);
469
+ return rank;
470
+ }
471
+ function seatLabelPart(row, i) {
472
+ const rawStart = row.seatLabelStart ?? 1;
509
473
  const dir = row.seatNumbering?.direction ?? "ltr";
510
474
  const step = row.seatNumbering?.step ?? 1;
475
+ const scheme = row.seatNumbering?.scheme ?? "decimal";
476
+ const prefix = row.seatNumbering?.prefix ?? "";
477
+ const endAt = row.seatNumbering?.endAt;
511
478
  const n = row.seatCount;
512
- let seatNumber;
513
- if (dir === "center") {
514
- const rank = new Array(n);
515
- Array.from({ length: n }, (_, i) => i).sort((a, b) => Math.abs(2 * a - (n - 1)) - Math.abs(2 * b - (n - 1)) || a - b).forEach((idx, k) => rank[idx] = k);
516
- seatNumber = (i) => start + rank[i] * step;
517
- } else {
518
- seatNumber = (i) => start + (dir === "rtl" ? n - 1 - i : i) * step;
479
+ if (scheme === "updown") {
480
+ const half = Math.ceil(n / 2);
481
+ const core2 = i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i);
482
+ return `${prefix}${core2}`;
483
+ }
484
+ const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
485
+ const start = endAt != null && Number.isFinite(endAt) ? endAt - (n - 1) * effStep : rawStart;
486
+ const p = dir === "center" ? centerRank(n)[i] : dir === "rtl" ? n - 1 - i : i;
487
+ let core;
488
+ switch (scheme) {
489
+ case "odd": {
490
+ const firstOdd = start % 2 === 1 ? start : start + 1;
491
+ core = String(firstOdd + p * 2);
492
+ break;
493
+ }
494
+ case "even": {
495
+ const firstEven = start % 2 === 0 ? start : start + 1;
496
+ core = String(firstEven + p * 2);
497
+ break;
498
+ }
499
+ case "roman":
500
+ core = toRoman(start + p * step);
501
+ break;
502
+ case "letters-upper":
503
+ core = toLetters(start + p * step, false);
504
+ break;
505
+ case "letters-lower":
506
+ core = toLetters(start + p * step, true);
507
+ break;
508
+ case "decimal":
509
+ default:
510
+ core = String(start + p * step);
511
+ break;
519
512
  }
513
+ return `${prefix}${core}`;
514
+ }
515
+ function expandRowSlots(row) {
520
516
  const ov = overrideMap(row);
521
517
  return rowSeatPositions(row).map((p, i) => {
522
518
  const o = ov.get(i);
523
519
  const accessibility = overrideAccessibility(o);
524
- const inventoryLabel = o?.label ?? `${row.label}-${seatNumber(i)}`;
520
+ const part = seatLabelPart(row, i);
521
+ const inventoryLabel = o?.label ?? `${row.label}-${part}`;
525
522
  const displayPrefix = row.displayLabel ?? row.label;
526
523
  const commercial = { ...row.commercial, ...o?.commercial };
527
524
  return {
@@ -529,7 +526,7 @@ function expandRowSlots(row) {
529
526
  x: p.x + (o?.dx ?? 0),
530
527
  y: p.y + (o?.dy ?? 0),
531
528
  label: inventoryLabel,
532
- displayLabel: o?.displayLabel ?? `${displayPrefix}-${seatNumber(i)}`,
529
+ displayLabel: o?.displayLabel ?? `${displayPrefix}-${part}`,
533
530
  categoryKey: o?.categoryKey ?? row.categoryKey,
534
531
  skipped: !!o?.skip,
535
532
  accessible: accessibility.length > 0,
@@ -671,8 +668,8 @@ function polygonLabelPoint(outer, holes) {
671
668
  const xs = outer.map((point) => point.x);
672
669
  const ys = outer.map((point) => point.y);
673
670
  const bounds = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
674
- const centroid = polygonCentroid(outer);
675
- if (pointInPolygonWithHoles(centroid, outer, holes)) return centroid;
671
+ const centroid2 = polygonCentroid(outer);
672
+ if (pointInPolygonWithHoles(centroid2, outer, holes)) return centroid2;
676
673
  let best = outer[0];
677
674
  let bestScore = -Infinity;
678
675
  const rings = [outer, ...holes ?? []];
@@ -996,6 +993,7 @@ var import_Rect = require("konva/lib/shapes/Rect");
996
993
  var import_Ellipse = require("konva/lib/shapes/Ellipse");
997
994
  var import_Line = require("konva/lib/shapes/Line");
998
995
  var import_Text = require("konva/lib/shapes/Text");
996
+ var import_Path = require("konva/lib/shapes/Path");
999
997
  var import_Image = require("konva/lib/shapes/Image");
1000
998
  var import_Shape = require("konva/lib/Shape");
1001
999
 
@@ -1012,6 +1010,8 @@ var LIGHT_BOOKABLE_LABEL_INK = "#ffffff";
1012
1010
  function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
1013
1011
  return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
1014
1012
  }
1013
+ var ACCESS_GLYPH_VIEWBOX = 24;
1014
+ var ACCESS_GLYPH_PATH = "M12 2c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 7h-6v13h-2v-6h-2v6H9V9H3V7h18v2z";
1015
1015
  function bookableMarkerLabel(publicLabel) {
1016
1016
  return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
1017
1017
  }
@@ -1091,29 +1091,17 @@ var en = {
1091
1091
  "common.done": "Done",
1092
1092
  "common.copied": "\u2713 Copied",
1093
1093
  // buyer picker
1094
- "picker.holdSeats": "Hold seats & checkout",
1095
- "picker.completeBooking": "Complete booking",
1096
- "picker.seatsHeld": "Seats held \u2014 {time}",
1097
1094
  "picker.holdExpired": "Your hold expired \u2014 the seats were released. Pick again.",
1098
- "picker.seatTaken": "Seat {label} was just taken by another buyer.",
1099
1095
  "picker.poweredBy": "Powered by SeatLayer",
1100
1096
  "picker.testMode": "TEST MODE",
1101
- "picker.colorblind": "Colorblind-friendly colors",
1102
1097
  "picker.orphanHint": "This leaves a single seat stranded \u2014 consider shifting one seat over.",
1103
- "picker.seats.one": "{count} seat",
1104
- "picker.seats.other": "{count} seats",
1105
1098
  // renderer (drawn on the Konva map — shared by the embed SDK)
1106
1099
  "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
1107
1100
  "map.seatsLeft": "{count} LEFT",
1108
1101
  "map.fromPrice": "FROM {price}",
1109
1102
  "map.statusHeld": "On hold",
1110
1103
  "map.statusTaken": "Taken",
1111
- // buyer picker page (src/pages/PickerPage.tsx)
1112
- "picker.language": "Language",
1113
- "picker.zoomToFit": "Zoom to fit",
1114
- "picker.seatCountLabel": "seats",
1115
- "picker.capacity": "capacity",
1116
- "picker.viewMode": "View mode",
1104
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
1117
1105
  "picker.floor": "Floor",
1118
1106
  "picker.zoomLevel": "Zoom level",
1119
1107
  "picker.rungTip.zones": "Venue overview \u2014 groups of sections such as North Stand or VIP",
@@ -1128,37 +1116,19 @@ var en = {
1128
1116
  "picker.seatsLeftInSection.other": "{count} seats left",
1129
1117
  "picker.overview": "Overview",
1130
1118
  "picker.tapSeatHint": "Tap any seat to check its view",
1131
- "picker.chartSize": "Chart size",
1132
- "picker.custom": "Custom",
1133
- "picker.categories": "Categories",
1134
- "picker.accessibility": "Accessibility",
1135
- "picker.showAnyAccessible": "Show any accessible seat",
1136
- "picker.any": "Any",
1137
- "picker.yourSeats": "Your seats",
1138
- "picker.emptySeats": "Tap seats on the map",
1139
- "picker.emptySeatsWithGa": "Tap seats on the map \xB7 tap a standing area for GA tickets",
1140
- "picker.oneFewer": "One fewer",
1141
- "picker.oneMore": "One more",
1142
- "picker.remove": "Remove {label}",
1143
1119
  "picker.ticketTierFor": "Ticket tier for {label}",
1144
- "picker.total": "Total: {amount}",
1145
1120
  "picker.viewFromSeat": "View from seat {label}",
1146
1121
  "picker.real360": "REAL 360\xB0",
1147
1122
  "picker.preview": "PREVIEW",
1148
- "picker.open360": "Open 360\xB0 view",
1149
1123
  "picker.sightline": "\u2248 {m} m to stage \xB7 clear sightline",
1150
- "picker.bookedDemo": "Booked! (demo)",
1151
- "picker.bookButton.one": "Book {count} ticket \u2014 {amount}",
1152
- "picker.bookButton.other": "Book {count} tickets \u2014 {amount}",
1153
- "picker.simulateCrowd": "Simulate crowd: {state}",
1154
- "picker.on": "ON",
1155
- "picker.off": "OFF",
1156
1124
  "picker.panorama360": "360\xB0 venue photo",
1157
1125
  "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m from stage",
1158
1126
  "picker.restrictedView": "Restricted view",
1159
1127
  "picker.obstructedView": "Obstructed view",
1160
1128
  "picker.premiumSeat": "Premium seat",
1161
- "picker.hideLimitedView": "Hide limited-view seats"
1129
+ "picker.hideLimitedView": "Hide limited-view seats",
1130
+ "picker.bestSeatsPremium": "Best seats",
1131
+ "picker.premiumFallbackNote": "No premium block of {count} \u2014 showing best overall"
1162
1132
  };
1163
1133
 
1164
1134
  // src/i18n/index.ts
@@ -1209,6 +1179,7 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
1209
1179
  var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
1210
1180
  var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
1211
1181
  var SEAT_TAP_SLOP_PX = 14;
1182
+ var SEAT_GLYPH_MIN_PX = 6.5;
1212
1183
  var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
1213
1184
  var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
1214
1185
  var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
@@ -1498,6 +1469,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1498
1469
  this.boothLabelById = /* @__PURE__ */ new Map();
1499
1470
  /** Viewport seat labels are rebuilt after each settled camera change. */
1500
1471
  this.seatLabelById = /* @__PURE__ */ new Map();
1472
+ /** Coloured accommodation ring per accessible seat (few per chart). */
1473
+ this.accessRingById = /* @__PURE__ */ new Map();
1474
+ /** Centred accessibility glyph per accessible seat — shown once the seat is
1475
+ * big enough on-screen (see {@link SEAT_GLYPH_MIN_PX}); the ring is the
1476
+ * smaller-zoom fallback. Kept in a map so zoom toggles touch only the handful
1477
+ * of accessible seats, never all 13k nodes. */
1478
+ this.accessGlyphById = /* @__PURE__ */ new Map();
1479
+ /** Whether the accessibility glyph is legible at the current camera scale. */
1480
+ this.accessGlyphVisible = false;
1501
1481
  /** Authored free-text nodes obey the same rendered-size visibility floor. */
1502
1482
  this.freeTextById = /* @__PURE__ */ new Map();
1503
1483
  /** Stage/rink landmarks retain a readable screen-space caption at overview. */
@@ -1810,6 +1790,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1810
1790
  this.boothDims.clear();
1811
1791
  this.boothLabelById.clear();
1812
1792
  this.seatLabelById.clear();
1793
+ this.accessRingById.clear();
1794
+ this.accessGlyphById.clear();
1813
1795
  this.freeTextById.clear();
1814
1796
  this.primaryFocalLabels.clear();
1815
1797
  this.gaById.clear();
@@ -2718,18 +2700,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2718
2700
  this.paintSeat(c, seat.id);
2719
2701
  target.add(c);
2720
2702
  if (seat.accessible) {
2721
- target.add(
2722
- new import_Circle.Circle({
2723
- x: seat.x,
2724
- y: seat.y,
2725
- radius: this.seatR + 1,
2726
- stroke: accessibilityRingColor(seat.accessibility),
2727
- strokeWidth: 2,
2728
- listening: false,
2729
- perfectDrawEnabled: false,
2730
- shadowForStrokeEnabled: false
2731
- })
2732
- );
2703
+ const ring = new import_Circle.Circle({
2704
+ x: seat.x,
2705
+ y: seat.y,
2706
+ radius: this.seatR + 1.5,
2707
+ stroke: accessibilityRingColor(seat.accessibility),
2708
+ strokeWidth: 2.5,
2709
+ listening: false,
2710
+ perfectDrawEnabled: false,
2711
+ shadowForStrokeEnabled: false
2712
+ });
2713
+ this.accessRingById.set(seat.id, ring);
2714
+ target.add(ring);
2715
+ const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
2716
+ this.accessGlyphById.set(seat.id, glyph);
2717
+ target.add(glyph);
2733
2718
  }
2734
2719
  }
2735
2720
  }
@@ -2773,6 +2758,53 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2773
2758
  target.add(t2);
2774
2759
  this.paintSeat(rect, seat.id);
2775
2760
  }
2761
+ /**
2762
+ * Build the centred accessibility glyph for a seat. Sized relative to the seat
2763
+ * radius (so it always fills the marker at any zoom) and given a contrast-aware
2764
+ * fill against the seat's paint — recomputed per state in {@link paintSeat}.
2765
+ */
2766
+ buildAccessGlyph(seat, seatFill) {
2767
+ const k = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX;
2768
+ return new import_Path.Path({
2769
+ x: seat.x,
2770
+ y: seat.y,
2771
+ data: ACCESS_GLYPH_PATH,
2772
+ offsetX: ACCESS_GLYPH_VIEWBOX / 2,
2773
+ offsetY: ACCESS_GLYPH_VIEWBOX / 2,
2774
+ scaleX: k,
2775
+ scaleY: k,
2776
+ fill: stateAwareBookableLabelInk(seatFill, "#ffffff"),
2777
+ listening: false,
2778
+ visible: this.accessGlyphVisible,
2779
+ perfectDrawEnabled: false,
2780
+ shadowForStrokeEnabled: false
2781
+ });
2782
+ }
2783
+ /**
2784
+ * Toggle the accessibility glyphs for the current camera scale: shown once the
2785
+ * effective on-screen seat radius clears {@link SEAT_GLYPH_MIN_PX}, otherwise
2786
+ * hidden so only the ring remains. Iterates the (small) accessible-seat set,
2787
+ * never the full node graph, so it is cheap to call on every view change.
2788
+ */
2789
+ updateAccessGlyphs(scale) {
2790
+ if (!this.accessGlyphById.size) return;
2791
+ this.accessGlyphVisible = this.seatR * scale >= SEAT_GLYPH_MIN_PX;
2792
+ for (const [id, glyph] of this.accessGlyphById) {
2793
+ glyph.visible(this.accessGlyphVisible && this.accessGlyphEligible(id));
2794
+ }
2795
+ }
2796
+ /**
2797
+ * Whether an accessible seat's glyph should show for its current status. It is
2798
+ * hidden on seats a buyer cannot take (sold, or another buyer's hold) where the
2799
+ * overlay status cue (diagonal mark / lock) carries the state instead — mirrors
2800
+ * the `unavailable` test in {@link updateLabels} so the two never collide.
2801
+ */
2802
+ accessGlyphEligible(id) {
2803
+ const status = this.statusById.get(id) ?? "free";
2804
+ if (status === "booked") return false;
2805
+ if (status === "held" && !this.ownedHold.has(id) && !this.opts.manageMode) return false;
2806
+ return true;
2807
+ }
2776
2808
  /** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
2777
2809
  seatBaseColor(categoryKey) {
2778
2810
  if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
@@ -2886,6 +2918,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2886
2918
  isBookableLabelLegibleAtScale(bookableLabel.fontSize(), this.effScale()) && c.opacity() >= 0.5
2887
2919
  );
2888
2920
  }
2921
+ const accessGlyph = this.accessGlyphById.get(id);
2922
+ if (accessGlyph) {
2923
+ const fill = c.fill();
2924
+ accessGlyph.fill(stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", "#ffffff"));
2925
+ accessGlyph.opacity(c.opacity());
2926
+ accessGlyph.visible(this.accessGlyphVisible && this.accessGlyphEligible(id));
2927
+ }
2928
+ this.accessRingById.get(id)?.opacity(c.opacity());
2889
2929
  }
2890
2930
  /** True when a seat sits in a section/zone currently marked `closed`. */
2891
2931
  seatInClosedSection(id) {
@@ -3364,7 +3404,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3364
3404
  * Membership and category mix are still precomputed for the detailed state.
3365
3405
  */
3366
3406
  renderSection(obj) {
3367
- const centroid = polygonLabelPoint(obj.outline, obj.holes);
3407
+ const centroid2 = polygonLabelPoint(obj.outline, obj.holes);
3368
3408
  const palette = overviewPalette(this.canvasBackground);
3369
3409
  const memberIds = [];
3370
3410
  const catCounts = /* @__PURE__ */ new Map();
@@ -3426,8 +3466,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3426
3466
  const preferredInk = labelStyle?.color ?? palette.sectionInk;
3427
3467
  const labelScale = (labelStyle?.size ?? 18) / 18;
3428
3468
  const nameLabel = new import_Text.Text({
3429
- x: obj.labelPresentation?.position?.x ?? centroid.x,
3430
- y: obj.labelPresentation?.position?.y ?? centroid.y,
3469
+ x: obj.labelPresentation?.position?.x ?? centroid2.x,
3470
+ y: obj.labelPresentation?.position?.y ?? centroid2.y,
3431
3471
  text: obj.displayLabel ?? obj.label,
3432
3472
  rotation: obj.labelPresentation?.rotation ?? 0,
3433
3473
  visible: obj.labelPresentation?.visible !== false,
@@ -3442,8 +3482,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3442
3482
  nameLabel.offsetY(nameLabel.height() / 2);
3443
3483
  bgTarget.add(nameLabel);
3444
3484
  const subLabel = new import_Text.Text({
3445
- x: centroid.x,
3446
- y: centroid.y,
3485
+ x: centroid2.x,
3486
+ y: centroid2.y,
3447
3487
  text: t("map.seatsLeft", { count: free }),
3448
3488
  fontSize: 12,
3449
3489
  fontStyle: "700",
@@ -3462,8 +3502,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3462
3502
  outline: obj.outline,
3463
3503
  ...obj.outlinePath ? { outlinePath: obj.outlinePath } : {},
3464
3504
  holes: obj.holes ?? [],
3465
- centroid,
3466
- labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [], centroid),
3505
+ centroid: centroid2,
3506
+ labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [], centroid2),
3467
3507
  zone: obj.zone,
3468
3508
  memberIds,
3469
3509
  total: memberIds.length,
@@ -4611,6 +4651,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4611
4651
  if (this.hasSections) this.applySectionLod(scale);
4612
4652
  else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
4613
4653
  this.paintGAStateForView();
4654
+ this.updateAccessGlyphs(scale);
4614
4655
  const shouldCache = scale < CACHE_THRESHOLD;
4615
4656
  if (shouldCache && !this.cached) {
4616
4657
  this.cacheSeatLayer();
@@ -4693,6 +4734,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4693
4734
  for (const seat of this.seats) {
4694
4735
  if (seat.kind === "booth") continue;
4695
4736
  if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
4737
+ if (seat.accessible && this.accessGlyphVisible) continue;
4696
4738
  const shape = this.circleById.get(seat.id);
4697
4739
  if ((shape?.opacity() ?? 1) < 0.5) continue;
4698
4740
  const status = this.statusById.get(seat.id) ?? "free";
@@ -4823,6 +4865,103 @@ function strandedSingles(seats, statusOf, selectedIds) {
4823
4865
  return out;
4824
4866
  }
4825
4867
 
4868
+ // src/core/bestAvailable.ts
4869
+ function seatIndex2(seat, fallback) {
4870
+ const i = seat.id.lastIndexOf(":");
4871
+ if (i < 0) return fallback;
4872
+ const n = Number(seat.id.slice(i + 1));
4873
+ return Number.isFinite(n) ? n : fallback;
4874
+ }
4875
+ function dist2(a, b) {
4876
+ const dx = a.x - b.x;
4877
+ const dy = a.y - b.y;
4878
+ return dx * dx + dy * dy;
4879
+ }
4880
+ function centroid(seats) {
4881
+ let x = 0;
4882
+ let y = 0;
4883
+ for (const s of seats) {
4884
+ x += s.x;
4885
+ y += s.y;
4886
+ }
4887
+ return { x: x / seats.length, y: y / seats.length };
4888
+ }
4889
+ function isPremium(seat) {
4890
+ return seat.commercial?.premium === true;
4891
+ }
4892
+ function pickBestAvailable(seats, available, opts) {
4893
+ const qty = Math.floor(opts.qty);
4894
+ if (!Number.isFinite(qty) || qty <= 0) return { labels: [], reason: "sold_out" };
4895
+ const { categoryKey, focal, preferPremium } = opts;
4896
+ const rows = /* @__PURE__ */ new Map();
4897
+ const eligibleAll = [];
4898
+ seats.forEach((seat, i) => {
4899
+ const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey);
4900
+ let arr = rows.get(seat.rowId);
4901
+ if (!arr) {
4902
+ arr = [];
4903
+ rows.set(seat.rowId, arr);
4904
+ }
4905
+ arr.push({ seat, index: seatIndex2(seat, i), elig });
4906
+ if (elig) eligibleAll.push(seat);
4907
+ });
4908
+ if (eligibleAll.length === 0) return { labels: [], reason: "sold_out" };
4909
+ let best = null;
4910
+ const better = (c) => {
4911
+ if (!best) return true;
4912
+ if (preferPremium && c.nonPremium !== best.nonPremium) return c.nonPremium < best.nonPremium;
4913
+ if (c.orphan !== best.orphan) return c.orphan < best.orphan;
4914
+ if (c.d2 !== best.d2) return c.d2 < best.d2;
4915
+ const r = c.rowId.localeCompare(best.rowId);
4916
+ if (r !== 0) return r < 0;
4917
+ return c.startIndex < best.startIndex;
4918
+ };
4919
+ for (const [rowId, slotsUnsorted] of rows) {
4920
+ const slots = [...slotsUnsorted].sort((a, b) => a.index - b.index);
4921
+ let i = 0;
4922
+ while (i < slots.length) {
4923
+ if (!slots[i].elig) {
4924
+ i++;
4925
+ continue;
4926
+ }
4927
+ let j = i;
4928
+ while (j < slots.length && slots[j].elig) j++;
4929
+ const segLen = j - i;
4930
+ for (let p = 0; p + qty <= segLen; p++) {
4931
+ const leftRem = p;
4932
+ const rightRem = segLen - (p + qty);
4933
+ const orphan = leftRem === 1 || rightRem === 1 ? 1 : 0;
4934
+ const runSeats = slots.slice(i + p, i + p + qty).map((s) => s.seat);
4935
+ const c = {
4936
+ labels: runSeats.map((s) => s.label),
4937
+ seats: runSeats,
4938
+ rowId,
4939
+ startIndex: slots[i + p].index,
4940
+ orphan,
4941
+ nonPremium: preferPremium ? runSeats.reduce((n, s) => n + (isPremium(s) ? 0 : 1), 0) : 0,
4942
+ d2: dist2(centroid(runSeats), focal)
4943
+ };
4944
+ if (better(c)) best = c;
4945
+ }
4946
+ i = j;
4947
+ }
4948
+ }
4949
+ if (best) return { labels: best.labels };
4950
+ if (eligibleAll.length < qty) return { labels: [], reason: "not_enough_together" };
4951
+ const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, focal) })).sort((a, b) => {
4952
+ if (preferPremium) {
4953
+ const pa = isPremium(a.seat) ? 0 : 1;
4954
+ const pb = isPremium(b.seat) ? 0 : 1;
4955
+ if (pa !== pb) return pa - pb;
4956
+ }
4957
+ if (a.d2 !== b.d2) return a.d2 - b.d2;
4958
+ const r = a.seat.rowId.localeCompare(b.seat.rowId);
4959
+ if (r !== 0) return r;
4960
+ return seatIndex2(a.seat, a.i) - seatIndex2(b.seat, b.i);
4961
+ });
4962
+ return { labels: ranked.slice(0, qty).map((r) => r.seat.label) };
4963
+ }
4964
+
4826
4965
  // src/picker/PickerController.ts
4827
4966
  var DEFAULT_MAX_SELECTION = 10;
4828
4967
  var MAX_BACKOFF_MS = 15e3;
@@ -4945,9 +5084,11 @@ var PickerController = class {
4945
5084
  this.allIds.push(s.id);
4946
5085
  const source = chartObjects.get(s.rowId);
4947
5086
  const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
4948
- const rowLabel = s.kind === "booth" ? void 0 : sourceLabel;
4949
- const labelParts = s.label.split("-");
4950
- const seatNumber = rowLabel && s.label.startsWith(`${rowLabel}-`) ? s.label.slice(rowLabel.length + 1) : s.kind === "booth" ? s.label : labelParts[labelParts.length - 1] ?? s.label;
5087
+ const sourceDisplayLabel = source && "displayLabel" in source && typeof source.displayLabel === "string" && source.displayLabel ? source.displayLabel : sourceLabel;
5088
+ const rowLabel = s.kind === "booth" ? void 0 : sourceDisplayLabel;
5089
+ const visibleSeatLabel = s.displayLabel ?? s.label;
5090
+ const labelParts = visibleSeatLabel.split("-");
5091
+ const seatNumber = sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) : s.kind === "booth" ? visibleSeatLabel : labelParts[labelParts.length - 1] ?? visibleSeatLabel;
4951
5092
  this.seatContext.set(s.id, {
4952
5093
  sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
4953
5094
  rowLabel,
@@ -5174,10 +5315,77 @@ var PickerController = class {
5174
5315
  this.setHold({ holdId: result.holdId, labels: combined.map((s) => s.label), expiresAt: result.expiresAt, items: result.items });
5175
5316
  return this.hold_;
5176
5317
  }
5177
- /** Server-picks `qty` best free seats and holds them atomically. Throws on failure. */
5178
- async bestAvailable(qty, categoryKey) {
5318
+ /** True when any seat on the visible chart carries the `premium` commercial
5319
+ * flag — gates the buyer widget's "★ Best seats" premium quick-pick (chip is
5320
+ * present-only, exactly like the accessibility filter chips). */
5321
+ hasPremiumSeats() {
5322
+ for (const seat of this.labelToSeat.values()) {
5323
+ if (seat.commercial?.premium) return true;
5324
+ }
5325
+ return false;
5326
+ }
5327
+ /**
5328
+ * Client-side premium pre-pass: find the best contiguous block of `qty`
5329
+ * PREMIUM-flagged free seats (orphan-avoiding, closest to the focal point)
5330
+ * using the local seat geometry + live availability, mirroring the server's
5331
+ * held-back exclusion via the visible (hidden-stripped) chart and current
5332
+ * seat status. Returns a FULL premium block of `qty`, or null when none
5333
+ * exists (the caller then falls back to the normal server pick).
5334
+ */
5335
+ pickPremiumBlock(qty, categoryKey) {
5336
+ const doc = this.visibleDoc();
5337
+ if (!doc?.objects?.length) return null;
5338
+ const focal = doc.focalPoint ?? { x: 0, y: 0 };
5339
+ const seats = expandChart(doc);
5340
+ const held = new Set(this.hold_?.labels ?? []);
5341
+ const available = /* @__PURE__ */ new Set();
5342
+ for (const seat of seats) {
5343
+ if (held.has(seat.label)) continue;
5344
+ const id = this.labelToId.get(seat.label);
5345
+ const status = id ? this.getStatus(id) : void 0;
5346
+ if ((status ?? "free") === "free") available.add(seat.label);
5347
+ }
5348
+ const pick = pickBestAvailable(seats, available, { qty, categoryKey, focal, preferPremium: true });
5349
+ if (pick.labels.length !== qty) return null;
5350
+ const allPremium = pick.labels.every((l) => this.labelToSeat.get(l)?.commercial?.premium);
5351
+ return allPremium ? [...pick.labels] : null;
5352
+ }
5353
+ /**
5354
+ * Server-picks `qty` best free seats and holds them atomically. Throws on
5355
+ * failure. With `preferPremium`, first tries to hold the best PREMIUM block
5356
+ * locally; when none matches the requested quantity it falls back to the
5357
+ * normal server pick (the widget surfaces a subtle note on that fallback).
5358
+ */
5359
+ async bestAvailable(qty, categoryKey, opts = {}) {
5179
5360
  const r = this.renderer;
5180
5361
  if (!r) return null;
5362
+ if (opts.preferPremium) {
5363
+ const block = this.pickPremiumBlock(qty, categoryKey);
5364
+ if (block) {
5365
+ if (this.hold_ && !await this.release()) return null;
5366
+ try {
5367
+ const selection = block.map((label) => {
5368
+ const seat = this.labelToSeat.get(label);
5369
+ const resolved = seat ? this.toSeat(seat) : null;
5370
+ return { label, ...resolved?.tierId ? { tierId: resolved.tierId } : {} };
5371
+ });
5372
+ const result = await this.api.hold(this.key, selection);
5373
+ r.clearSelection();
5374
+ const ids = block.map((l) => this.labelToId.get(l)).filter((v) => !!v);
5375
+ if (ids.length) r.setStatus(ids, "held");
5376
+ this.setHold({ holdId: result.holdId, labels: [...block], expiresAt: result.expiresAt, items: result.items });
5377
+ const seats = block.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
5378
+ this.opts.onSelectionChange?.(seats);
5379
+ return this.hold_;
5380
+ } catch (err) {
5381
+ const { status, reason } = errInfo(err);
5382
+ if (status === 409 && reason === "event_closed") {
5383
+ this.opts.onSalesClosed?.();
5384
+ throw err;
5385
+ }
5386
+ }
5387
+ }
5388
+ }
5181
5389
  if (this.hold_ && !await this.release()) return null;
5182
5390
  try {
5183
5391
  const result = await this.api.bestAvailable(this.key, qty, categoryKey);
@@ -5550,14 +5758,16 @@ var PickerController = class {
5550
5758
  // ---- internals ------------------------------------------------------------
5551
5759
  toSeat(s) {
5552
5760
  const commercial = s.commercial ? { commercial: s.commercial } : void 0;
5761
+ const display = s.displayLabel ? { displayLabel: s.displayLabel } : void 0;
5553
5762
  const tiers = this.tiersFor(s.categoryKey);
5554
5763
  if (!tiers) {
5555
- return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
5764
+ return { id: s.id, label: s.label, ...display, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
5556
5765
  }
5557
5766
  const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
5558
5767
  return {
5559
5768
  id: s.id,
5560
5769
  label: s.label,
5770
+ ...display,
5561
5771
  categoryKey: s.categoryKey,
5562
5772
  price: chosen.price,
5563
5773
  tiers,
@@ -6118,6 +6328,8 @@ async function loadLocale(code) {
6118
6328
  pointInPolygonWithHoles,
6119
6329
  polygonLabelPoint,
6120
6330
  resolveLocale,
6331
+ rowSeatPositions,
6332
+ seatLabelPart,
6121
6333
  setLocale,
6122
6334
  setMoneyLocale,
6123
6335
  setStringOverrides,