@seatlayer/core 0.23.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,31 +66,11 @@ 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",
@@ -131,29 +99,17 @@ var init_de = __esm({
131
99
  "common.done": "Fertig",
132
100
  "common.copied": "\u2713 Kopiert",
133
101
  // buyer picker (extraction in progress — P4 of the port plan)
134
- "picker.holdSeats": "Pl\xE4tze reservieren & Kasse",
135
- "picker.completeBooking": "Buchung abschlie\xDFen",
136
- "picker.seatsHeld": "Pl\xE4tze reserviert \u2014 {time}",
137
102
  "picker.holdExpired": "Ihre Reservierung ist abgelaufen \u2014 die Pl\xE4tze wurden freigegeben. Bitte erneut ausw\xE4hlen.",
138
- "picker.seatTaken": "Platz {label} wurde gerade von einem anderen K\xE4ufer gebucht.",
139
103
  "picker.poweredBy": "Bereitgestellt von SeatLayer",
140
104
  "picker.testMode": "TESTMODUS",
141
- "picker.colorblind": "Farbenblind-freundliche Farben",
142
105
  "picker.orphanHint": "Dadurch bleibt ein einzelner Platz \xFCbrig \u2014 r\xFCcken Sie ggf. einen Platz weiter.",
143
- "picker.seats.one": "{count} Platz",
144
- "picker.seats.other": "{count} Pl\xE4tze",
145
106
  // renderer (drawn on the Konva map — shared by the embed SDK)
146
107
  "map.aria": "Sitzplan. Mit Pfeiltasten zwischen Pl\xE4tzen navigieren, Eingabe zum Ausw\xE4hlen.",
147
108
  "map.seatsLeft": "{count} FREI",
148
109
  "map.statusHeld": "Reserviert",
149
110
  "map.statusTaken": "Vergeben",
150
111
  "map.fromPrice": "AB {price}",
151
- // buyer picker page (src/pages/PickerPage.tsx)
152
- "picker.language": "Sprache",
153
- "picker.zoomToFit": "Auf Gr\xF6\xDFe anpassen",
154
- "picker.seatCountLabel": "Pl\xE4tze",
155
- "picker.capacity": "Kapazit\xE4t",
156
- "picker.viewMode": "Ansichtsmodus",
112
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
157
113
  "picker.floor": "Etage",
158
114
  "picker.zoomLevel": "Zoomstufe",
159
115
  "picker.rungTip.zones": "\xDCbersicht \u2014 Bereichsgruppen wie Nordtrib\xFCne oder VIP",
@@ -168,31 +124,11 @@ var init_de = __esm({
168
124
  "picker.seatsLeftInSection.other": "{count} Pl\xE4tze verf\xFCgbar",
169
125
  "picker.overview": "\xDCbersicht",
170
126
  "picker.tapSeatHint": "Tippen Sie auf einen Platz, um die Ansicht zu pr\xFCfen",
171
- "picker.chartSize": "Kartengr\xF6\xDFe",
172
- "picker.custom": "Benutzerdefiniert",
173
- "picker.categories": "Kategorien",
174
- "picker.accessibility": "Barrierefreiheit",
175
- "picker.showAnyAccessible": "Jeden barrierefreien Platz anzeigen",
176
- "picker.any": "Beliebig",
177
- "picker.yourSeats": "Ihre Pl\xE4tze",
178
- "picker.emptySeats": "Tippen Sie auf die Pl\xE4tze auf der Karte",
179
- "picker.emptySeatsWithGa": "Tippen Sie auf die Pl\xE4tze auf der Karte \xB7 tippen Sie auf einen Stehbereich f\xFCr Stehplatztickets",
180
- "picker.oneFewer": "Einen weniger",
181
- "picker.oneMore": "Einen mehr",
182
- "picker.remove": "{label} entfernen",
183
127
  "picker.ticketTierFor": "Ticketklasse f\xFCr {label}",
184
- "picker.total": "Gesamt: {amount}",
185
128
  "picker.viewFromSeat": "Ansicht von Platz {label}",
186
129
  "picker.real360": "REAL 360\xB0",
187
130
  "picker.preview": "VORSCHAU",
188
- "picker.open360": "\xD6ffnen Sie die 360\xB0-Ansicht",
189
131
  "picker.sightline": "\u2248 {m} m zur B\xFChne \xB7 freie Sicht",
190
- "picker.bookedDemo": "Gebucht! (Demo)",
191
- "picker.bookButton.one": "{count} Ticket buchen \u2014 {amount}",
192
- "picker.bookButton.other": "{count} Tickets buchen \u2014 {amount}",
193
- "picker.simulateCrowd": "Menschenmenge simulieren: {state}",
194
- "picker.on": "AN",
195
- "picker.off": "AUS",
196
132
  "picker.panorama360": "360\xB0-Veranstaltungsfotos",
197
133
  "picker.illustrationCaption": "Illustration \xB7 \u2248 {m} m von der B\xFChne",
198
134
  "picker.restrictedView": "Eingeschr\xE4nkte Sicht",
@@ -221,29 +157,17 @@ var init_fr = __esm({
221
157
  "common.done": "Termin\xE9",
222
158
  "common.copied": "\u2713 Copi\xE9",
223
159
  // buyer picker (extraction in progress — P4 of the port plan)
224
- "picker.holdSeats": "R\xE9server les si\xE8ges et valider",
225
- "picker.completeBooking": "Terminer la r\xE9servation",
226
- "picker.seatsHeld": "Si\xE8ges r\xE9serv\xE9s \u2014 {time}",
227
160
  "picker.holdExpired": "Votre r\xE9servation a expir\xE9 \u2014 les si\xE8ges ont \xE9t\xE9 lib\xE9r\xE9s. S\xE9lectionnez \xE0 nouveau.",
228
- "picker.seatTaken": "Le si\xE8ge {label} vient d'\xEAtre r\xE9serv\xE9 par un autre acheteur.",
229
161
  "picker.poweredBy": "Propuls\xE9 par SeatLayer",
230
162
  "picker.testMode": "MODE TEST",
231
- "picker.colorblind": "Couleurs adapt\xE9es au daltonisme",
232
163
  "picker.orphanHint": "Cela laisse un si\xE8ge isol\xE9 \u2014 pensez \xE0 vous d\xE9caler d'un si\xE8ge.",
233
- "picker.seats.one": "{count} si\xE8ge",
234
- "picker.seats.other": "{count} si\xE8ges",
235
164
  // renderer (drawn on the Konva map — shared by the embed SDK)
236
165
  "map.aria": "Plan de salle. Utilisez les fl\xE8ches pour naviguer entre les si\xE8ges, Entr\xE9e pour s\xE9lectionner.",
237
166
  "map.seatsLeft": "{count} LIBRES",
238
167
  "map.statusHeld": "En attente",
239
168
  "map.statusTaken": "Pris",
240
169
  "map.fromPrice": "D\xC8S {price}",
241
- // buyer picker page (src/pages/PickerPage.tsx)
242
- "picker.language": "Langue",
243
- "picker.zoomToFit": "Ajuster le zoom",
244
- "picker.seatCountLabel": "si\xE8ges",
245
- "picker.capacity": "capacit\xE9",
246
- "picker.viewMode": "Mode d'affichage",
170
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
247
171
  "picker.floor": "\xC9tage",
248
172
  "picker.zoomLevel": "Niveau de zoom",
249
173
  "picker.rungTip.zones": "Vue g\xE9n\xE9rale \u2014 groupes de sections comme Tribune nord ou VIP",
@@ -258,31 +182,11 @@ var init_fr = __esm({
258
182
  "picker.seatsLeftInSection.other": "{count} si\xE8ges restants",
259
183
  "picker.overview": "Vue d'ensemble",
260
184
  "picker.tapSeatHint": "Appuyez sur un si\xE8ge pour v\xE9rifier sa vue",
261
- "picker.chartSize": "Taille du plan",
262
- "picker.custom": "Personnalis\xE9",
263
- "picker.categories": "Cat\xE9gories",
264
- "picker.accessibility": "Accessibilit\xE9",
265
- "picker.showAnyAccessible": "Afficher un si\xE8ge accessible",
266
- "picker.any": "N'importe quel",
267
- "picker.yourSeats": "Vos si\xE8ges",
268
- "picker.emptySeats": "Appuyez sur les si\xE8ges sur le plan",
269
- "picker.emptySeatsWithGa": "Appuyez sur les si\xE8ges sur le plan \xB7 appuyez sur une zone debout pour les billets GA",
270
- "picker.oneFewer": "Un de moins",
271
- "picker.oneMore": "Un de plus",
272
- "picker.remove": "Supprimer {label}",
273
185
  "picker.ticketTierFor": "Cat\xE9gorie de billet pour {label}",
274
- "picker.total": "Total : {amount}",
275
186
  "picker.viewFromSeat": "Vue depuis le si\xE8ge {label}",
276
187
  "picker.real360": "VRAI 360\xB0",
277
188
  "picker.preview": "APER\xC7U",
278
- "picker.open360": "Ouvrir la vue \xE0 360\xB0",
279
189
  "picker.sightline": "\u2248 {m} m de la sc\xE8ne \xB7 vue d\xE9gag\xE9e",
280
- "picker.bookedDemo": "R\xE9serv\xE9 ! (d\xE9mo)",
281
- "picker.bookButton.one": "R\xE9server {count} billet \u2014 {amount}",
282
- "picker.bookButton.other": "R\xE9server {count} billets \u2014 {amount}",
283
- "picker.simulateCrowd": "Simuler la foule : {state}",
284
- "picker.on": "ACTIV\xC9",
285
- "picker.off": "D\xC9SACTIV\xC9",
286
190
  "picker.panorama360": "Photo panoramique 360\xB0 du lieu",
287
191
  "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m de la sc\xE8ne",
288
192
  "picker.restrictedView": "Visibilit\xE9 r\xE9duite",
@@ -343,6 +247,8 @@ __export(index_exports, {
343
247
  pointInPolygonWithHoles: () => pointInPolygonWithHoles,
344
248
  polygonLabelPoint: () => polygonLabelPoint,
345
249
  resolveLocale: () => resolveLocale,
250
+ rowSeatPositions: () => rowSeatPositions,
251
+ seatLabelPart: () => seatLabelPart,
346
252
  setLocale: () => setLocale,
347
253
  setMoneyLocale: () => setMoneyLocale,
348
254
  setStringOverrides: () => setStringOverrides,
@@ -465,6 +371,53 @@ function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected
465
371
  };
466
372
  }
467
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
+
468
421
  // src/core/layout.ts
469
422
  function overrideAccessibility(o) {
470
423
  if (!o) return [];
@@ -510,24 +463,62 @@ function overrideMap(row) {
510
463
  if (row.overrides) for (const o of row.overrides) m.set(o.index, o);
511
464
  return m;
512
465
  }
513
- function expandRowSlots(row) {
514
- 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;
515
473
  const dir = row.seatNumbering?.direction ?? "ltr";
516
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;
517
478
  const n = row.seatCount;
518
- let seatNumber;
519
- if (dir === "center") {
520
- const rank = new Array(n);
521
- 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);
522
- seatNumber = (i) => start + rank[i] * step;
523
- } else {
524
- 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;
525
512
  }
513
+ return `${prefix}${core}`;
514
+ }
515
+ function expandRowSlots(row) {
526
516
  const ov = overrideMap(row);
527
517
  return rowSeatPositions(row).map((p, i) => {
528
518
  const o = ov.get(i);
529
519
  const accessibility = overrideAccessibility(o);
530
- const inventoryLabel = o?.label ?? `${row.label}-${seatNumber(i)}`;
520
+ const part = seatLabelPart(row, i);
521
+ const inventoryLabel = o?.label ?? `${row.label}-${part}`;
531
522
  const displayPrefix = row.displayLabel ?? row.label;
532
523
  const commercial = { ...row.commercial, ...o?.commercial };
533
524
  return {
@@ -535,7 +526,7 @@ function expandRowSlots(row) {
535
526
  x: p.x + (o?.dx ?? 0),
536
527
  y: p.y + (o?.dy ?? 0),
537
528
  label: inventoryLabel,
538
- displayLabel: o?.displayLabel ?? `${displayPrefix}-${seatNumber(i)}`,
529
+ displayLabel: o?.displayLabel ?? `${displayPrefix}-${part}`,
539
530
  categoryKey: o?.categoryKey ?? row.categoryKey,
540
531
  skipped: !!o?.skip,
541
532
  accessible: accessibility.length > 0,
@@ -1002,6 +993,7 @@ var import_Rect = require("konva/lib/shapes/Rect");
1002
993
  var import_Ellipse = require("konva/lib/shapes/Ellipse");
1003
994
  var import_Line = require("konva/lib/shapes/Line");
1004
995
  var import_Text = require("konva/lib/shapes/Text");
996
+ var import_Path = require("konva/lib/shapes/Path");
1005
997
  var import_Image = require("konva/lib/shapes/Image");
1006
998
  var import_Shape = require("konva/lib/Shape");
1007
999
 
@@ -1018,6 +1010,8 @@ var LIGHT_BOOKABLE_LABEL_INK = "#ffffff";
1018
1010
  function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
1019
1011
  return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
1020
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";
1021
1015
  function bookableMarkerLabel(publicLabel) {
1022
1016
  return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
1023
1017
  }
@@ -1097,29 +1091,17 @@ var en = {
1097
1091
  "common.done": "Done",
1098
1092
  "common.copied": "\u2713 Copied",
1099
1093
  // buyer picker
1100
- "picker.holdSeats": "Hold seats & checkout",
1101
- "picker.completeBooking": "Complete booking",
1102
- "picker.seatsHeld": "Seats held \u2014 {time}",
1103
1094
  "picker.holdExpired": "Your hold expired \u2014 the seats were released. Pick again.",
1104
- "picker.seatTaken": "Seat {label} was just taken by another buyer.",
1105
1095
  "picker.poweredBy": "Powered by SeatLayer",
1106
1096
  "picker.testMode": "TEST MODE",
1107
- "picker.colorblind": "Colorblind-friendly colors",
1108
1097
  "picker.orphanHint": "This leaves a single seat stranded \u2014 consider shifting one seat over.",
1109
- "picker.seats.one": "{count} seat",
1110
- "picker.seats.other": "{count} seats",
1111
1098
  // renderer (drawn on the Konva map — shared by the embed SDK)
1112
1099
  "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
1113
1100
  "map.seatsLeft": "{count} LEFT",
1114
1101
  "map.fromPrice": "FROM {price}",
1115
1102
  "map.statusHeld": "On hold",
1116
1103
  "map.statusTaken": "Taken",
1117
- // buyer picker page (src/pages/PickerPage.tsx)
1118
- "picker.language": "Language",
1119
- "picker.zoomToFit": "Zoom to fit",
1120
- "picker.seatCountLabel": "seats",
1121
- "picker.capacity": "capacity",
1122
- "picker.viewMode": "View mode",
1104
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
1123
1105
  "picker.floor": "Floor",
1124
1106
  "picker.zoomLevel": "Zoom level",
1125
1107
  "picker.rungTip.zones": "Venue overview \u2014 groups of sections such as North Stand or VIP",
@@ -1134,31 +1116,11 @@ var en = {
1134
1116
  "picker.seatsLeftInSection.other": "{count} seats left",
1135
1117
  "picker.overview": "Overview",
1136
1118
  "picker.tapSeatHint": "Tap any seat to check its view",
1137
- "picker.chartSize": "Chart size",
1138
- "picker.custom": "Custom",
1139
- "picker.categories": "Categories",
1140
- "picker.accessibility": "Accessibility",
1141
- "picker.showAnyAccessible": "Show any accessible seat",
1142
- "picker.any": "Any",
1143
- "picker.yourSeats": "Your seats",
1144
- "picker.emptySeats": "Tap seats on the map",
1145
- "picker.emptySeatsWithGa": "Tap seats on the map \xB7 tap a standing area for GA tickets",
1146
- "picker.oneFewer": "One fewer",
1147
- "picker.oneMore": "One more",
1148
- "picker.remove": "Remove {label}",
1149
1119
  "picker.ticketTierFor": "Ticket tier for {label}",
1150
- "picker.total": "Total: {amount}",
1151
1120
  "picker.viewFromSeat": "View from seat {label}",
1152
1121
  "picker.real360": "REAL 360\xB0",
1153
1122
  "picker.preview": "PREVIEW",
1154
- "picker.open360": "Open 360\xB0 view",
1155
1123
  "picker.sightline": "\u2248 {m} m to stage \xB7 clear sightline",
1156
- "picker.bookedDemo": "Booked! (demo)",
1157
- "picker.bookButton.one": "Book {count} ticket \u2014 {amount}",
1158
- "picker.bookButton.other": "Book {count} tickets \u2014 {amount}",
1159
- "picker.simulateCrowd": "Simulate crowd: {state}",
1160
- "picker.on": "ON",
1161
- "picker.off": "OFF",
1162
1124
  "picker.panorama360": "360\xB0 venue photo",
1163
1125
  "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m from stage",
1164
1126
  "picker.restrictedView": "Restricted view",
@@ -1217,6 +1179,7 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
1217
1179
  var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
1218
1180
  var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
1219
1181
  var SEAT_TAP_SLOP_PX = 14;
1182
+ var SEAT_GLYPH_MIN_PX = 6.5;
1220
1183
  var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
1221
1184
  var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
1222
1185
  var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
@@ -1506,6 +1469,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1506
1469
  this.boothLabelById = /* @__PURE__ */ new Map();
1507
1470
  /** Viewport seat labels are rebuilt after each settled camera change. */
1508
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;
1509
1481
  /** Authored free-text nodes obey the same rendered-size visibility floor. */
1510
1482
  this.freeTextById = /* @__PURE__ */ new Map();
1511
1483
  /** Stage/rink landmarks retain a readable screen-space caption at overview. */
@@ -1818,6 +1790,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1818
1790
  this.boothDims.clear();
1819
1791
  this.boothLabelById.clear();
1820
1792
  this.seatLabelById.clear();
1793
+ this.accessRingById.clear();
1794
+ this.accessGlyphById.clear();
1821
1795
  this.freeTextById.clear();
1822
1796
  this.primaryFocalLabels.clear();
1823
1797
  this.gaById.clear();
@@ -2726,18 +2700,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2726
2700
  this.paintSeat(c, seat.id);
2727
2701
  target.add(c);
2728
2702
  if (seat.accessible) {
2729
- target.add(
2730
- new import_Circle.Circle({
2731
- x: seat.x,
2732
- y: seat.y,
2733
- radius: this.seatR + 1,
2734
- stroke: accessibilityRingColor(seat.accessibility),
2735
- strokeWidth: 2,
2736
- listening: false,
2737
- perfectDrawEnabled: false,
2738
- shadowForStrokeEnabled: false
2739
- })
2740
- );
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);
2741
2718
  }
2742
2719
  }
2743
2720
  }
@@ -2781,6 +2758,53 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2781
2758
  target.add(t2);
2782
2759
  this.paintSeat(rect, seat.id);
2783
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
+ }
2784
2808
  /** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
2785
2809
  seatBaseColor(categoryKey) {
2786
2810
  if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
@@ -2894,6 +2918,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2894
2918
  isBookableLabelLegibleAtScale(bookableLabel.fontSize(), this.effScale()) && c.opacity() >= 0.5
2895
2919
  );
2896
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());
2897
2929
  }
2898
2930
  /** True when a seat sits in a section/zone currently marked `closed`. */
2899
2931
  seatInClosedSection(id) {
@@ -4619,6 +4651,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4619
4651
  if (this.hasSections) this.applySectionLod(scale);
4620
4652
  else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
4621
4653
  this.paintGAStateForView();
4654
+ this.updateAccessGlyphs(scale);
4622
4655
  const shouldCache = scale < CACHE_THRESHOLD;
4623
4656
  if (shouldCache && !this.cached) {
4624
4657
  this.cacheSeatLayer();
@@ -4701,6 +4734,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4701
4734
  for (const seat of this.seats) {
4702
4735
  if (seat.kind === "booth") continue;
4703
4736
  if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
4737
+ if (seat.accessible && this.accessGlyphVisible) continue;
4704
4738
  const shape = this.circleById.get(seat.id);
4705
4739
  if ((shape?.opacity() ?? 1) < 0.5) continue;
4706
4740
  const status = this.statusById.get(seat.id) ?? "free";
@@ -5050,9 +5084,11 @@ var PickerController = class {
5050
5084
  this.allIds.push(s.id);
5051
5085
  const source = chartObjects.get(s.rowId);
5052
5086
  const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
5053
- const rowLabel = s.kind === "booth" ? void 0 : sourceLabel;
5054
- const labelParts = s.label.split("-");
5055
- 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;
5056
5092
  this.seatContext.set(s.id, {
5057
5093
  sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
5058
5094
  rowLabel,
@@ -5722,14 +5758,16 @@ var PickerController = class {
5722
5758
  // ---- internals ------------------------------------------------------------
5723
5759
  toSeat(s) {
5724
5760
  const commercial = s.commercial ? { commercial: s.commercial } : void 0;
5761
+ const display = s.displayLabel ? { displayLabel: s.displayLabel } : void 0;
5725
5762
  const tiers = this.tiersFor(s.categoryKey);
5726
5763
  if (!tiers) {
5727
- 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 };
5728
5765
  }
5729
5766
  const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
5730
5767
  return {
5731
5768
  id: s.id,
5732
5769
  label: s.label,
5770
+ ...display,
5733
5771
  categoryKey: s.categoryKey,
5734
5772
  price: chosen.price,
5735
5773
  tiers,
@@ -6290,6 +6328,8 @@ async function loadLocale(code) {
6290
6328
  pointInPolygonWithHoles,
6291
6329
  polygonLabelPoint,
6292
6330
  resolveLocale,
6331
+ rowSeatPositions,
6332
+ seatLabelPart,
6293
6333
  setLocale,
6294
6334
  setMoneyLocale,
6295
6335
  setStringOverrides,