@seatlayer/core 0.1.2 → 0.1.4

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
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
6
9
  var __export = (target, all) => {
7
10
  for (var name in all)
8
11
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -17,24 +20,283 @@ var __copyProps = (to, from, except, desc) => {
17
20
  };
18
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
22
 
23
+ // src/i18n/locales/es.ts
24
+ var es_exports = {};
25
+ __export(es_exports, {
26
+ es: () => es
27
+ });
28
+ var es;
29
+ var init_es = __esm({
30
+ "src/i18n/locales/es.ts"() {
31
+ "use strict";
32
+ es = {
33
+ // shared
34
+ "common.cancel": "Cancelar",
35
+ "common.close": "Cerrar",
36
+ "common.done": "Listo",
37
+ "common.copied": "\u2713 Copiado",
38
+ // buyer picker (extraction in progress — P4 of the port plan)
39
+ "picker.holdSeats": "Retener asientos y comprar",
40
+ "picker.completeBooking": "Completar reserva",
41
+ "picker.seatsHeld": "Asientos retenidos \u2014 {time}",
42
+ "picker.holdExpired": "Tu retenci\xF3n expir\xF3 \u2014 los asientos fueron liberados. Elige de nuevo.",
43
+ "picker.seatTaken": "El asiento {label} fue tomado por otro comprador.",
44
+ "picker.poweredBy": "Con la tecnolog\xEDa de SeatLayer",
45
+ "picker.colorblind": "Colores aptos para daltonismo",
46
+ "picker.orphanHint": "Esto deja un asiento aislado \u2014 considera desplazarte un asiento.",
47
+ "picker.seats.one": "{count} asiento",
48
+ "picker.seats.other": "{count} asientos",
49
+ // renderer (drawn on the Konva map — shared by the embed SDK)
50
+ "map.aria": "Mapa de asientos. Usa las flechas para moverte entre asientos, Intro para seleccionar.",
51
+ "map.seatsLeft": "{count} LIBRES",
52
+ "map.fromPrice": "DESDE {price}",
53
+ // buyer picker page (src/pages/PickerPage.tsx)
54
+ "picker.language": "Idioma",
55
+ "picker.zoomToFit": "Ajustar zoom",
56
+ "picker.viewMode": "Modo de vista",
57
+ "picker.floor": "Piso",
58
+ "picker.zoomLevel": "Nivel de zoom",
59
+ "picker.rungTip.zones": "Nivel m\xE1s lejano \u2014 bloques de zona (Piso / Patio Bajo / Patio Alto)",
60
+ "picker.rungTip.sections": "Bloques de secci\xF3n \u2014 mezcla de categor\xEDas + matiz de disponibilidad",
61
+ "picker.rungTip.seats": "Asientos individuales \u2014 los bloques se convierten en puntos",
62
+ "picker.rungLabel.zones": "ZONAS",
63
+ "picker.rungLabel.sections": "SECCIONES",
64
+ "picker.rungLabel.seats": "ASIENTOS",
65
+ "picker.sectionSummaryAria": "Resumen de secci\xF3n {label}",
66
+ "picker.closeSectionSummary": "Cerrar resumen de secci\xF3n",
67
+ "picker.seatsLeftInSection.one": "{count} asiento disponible",
68
+ "picker.seatsLeftInSection.other": "{count} asientos disponibles",
69
+ "picker.overview": "Descripci\xF3n general",
70
+ "picker.tapSeatHint": "Toca cualquier asiento para ver su vista",
71
+ "picker.chartSize": "Tama\xF1o del gr\xE1fico",
72
+ "picker.custom": "Personalizado",
73
+ "picker.categories": "Categor\xEDas",
74
+ "picker.accessibility": "Accesibilidad",
75
+ "picker.showAnyAccessible": "Mostrar cualquier asiento accesible",
76
+ "picker.any": "Cualquiera",
77
+ "picker.yourSeats": "Tus asientos",
78
+ "picker.emptySeats": "Toca asientos en el mapa",
79
+ "picker.emptySeatsWithGa": "Toca asientos en el mapa \xB7 toca un \xE1rea de pie para entradas de entrada general",
80
+ "picker.oneFewer": "Uno menos",
81
+ "picker.oneMore": "Uno m\xE1s",
82
+ "picker.remove": "Eliminar {label}",
83
+ "picker.ticketTierFor": "Categor\xEDa de entrada para {label}",
84
+ "picker.total": "Total: {amount}",
85
+ "picker.viewFromSeat": "Vista desde el asiento {label}",
86
+ "picker.real360": "REAL 360\xB0",
87
+ "picker.preview": "VISTA PREVIA",
88
+ "picker.open360": "Abrir vista de 360\xB0",
89
+ "picker.sightline": "\u2248 {rows} filas del escenario \xB7 {angle}\xB0 fuera del centro",
90
+ "picker.bookedDemo": "\xA1Reservado! (demostraci\xF3n)",
91
+ "picker.bookButton.one": "Reservar {count} entrada \u2014 {amount}",
92
+ "picker.bookButton.other": "Reservar {count} entradas \u2014 {amount}",
93
+ "picker.simulateCrowd": "Simular multitud: {state}",
94
+ "picker.on": "ACTIVADO",
95
+ "picker.off": "DESACTIVADO",
96
+ "picker.panorama360": "Foto de 360\xB0 del lugar",
97
+ "picker.illustrationCaption": "ilustraci\xF3n \xB7 \u2248 {m} m del escenario"
98
+ };
99
+ }
100
+ });
101
+
102
+ // src/i18n/locales/de.ts
103
+ var de_exports = {};
104
+ __export(de_exports, {
105
+ de: () => de
106
+ });
107
+ var de;
108
+ var init_de = __esm({
109
+ "src/i18n/locales/de.ts"() {
110
+ "use strict";
111
+ de = {
112
+ // shared
113
+ "common.cancel": "Abbrechen",
114
+ "common.close": "Schlie\xDFen",
115
+ "common.done": "Fertig",
116
+ "common.copied": "\u2713 Kopiert",
117
+ // buyer picker (extraction in progress — P4 of the port plan)
118
+ "picker.holdSeats": "Pl\xE4tze reservieren & Kasse",
119
+ "picker.completeBooking": "Buchung abschlie\xDFen",
120
+ "picker.seatsHeld": "Pl\xE4tze reserviert \u2014 {time}",
121
+ "picker.holdExpired": "Ihre Reservierung ist abgelaufen \u2014 die Pl\xE4tze wurden freigegeben. Bitte erneut ausw\xE4hlen.",
122
+ "picker.seatTaken": "Platz {label} wurde gerade von einem anderen K\xE4ufer gebucht.",
123
+ "picker.poweredBy": "Bereitgestellt von SeatLayer",
124
+ "picker.colorblind": "Farbenblind-freundliche Farben",
125
+ "picker.orphanHint": "Dadurch bleibt ein einzelner Platz \xFCbrig \u2014 r\xFCcken Sie ggf. einen Platz weiter.",
126
+ "picker.seats.one": "{count} Platz",
127
+ "picker.seats.other": "{count} Pl\xE4tze",
128
+ // renderer (drawn on the Konva map — shared by the embed SDK)
129
+ "map.aria": "Sitzplan. Mit Pfeiltasten zwischen Pl\xE4tzen navigieren, Eingabe zum Ausw\xE4hlen.",
130
+ "map.seatsLeft": "{count} FREI",
131
+ "map.fromPrice": "AB {price}",
132
+ // buyer picker page (src/pages/PickerPage.tsx)
133
+ "picker.language": "Sprache",
134
+ "picker.zoomToFit": "Auf Gr\xF6\xDFe anpassen",
135
+ "picker.viewMode": "Ansichtsmodus",
136
+ "picker.floor": "Etage",
137
+ "picker.zoomLevel": "Zoomstufe",
138
+ "picker.rungTip.zones": "Fernste Stufe \u2014 Zonen-Bl\xF6cke (Etage / Unteres Parterre / Oberes Parterre)",
139
+ "picker.rungTip.sections": "Bereichs-Bl\xF6cke \u2014 Kategoriesortierung + Verf\xFCgbarkeitst\xF6nung",
140
+ "picker.rungTip.seats": "Einzelne Pl\xE4tze \u2014 Bl\xF6cke l\xF6sen sich zu Punkten auf",
141
+ "picker.rungLabel.zones": "ZONEN",
142
+ "picker.rungLabel.sections": "BEREICHE",
143
+ "picker.rungLabel.seats": "PL\xC4TZE",
144
+ "picker.sectionSummaryAria": "Bereichszusammenfassung {label}",
145
+ "picker.closeSectionSummary": "Bereichszusammenfassung schlie\xDFen",
146
+ "picker.seatsLeftInSection.one": "{count} Platz verf\xFCgbar",
147
+ "picker.seatsLeftInSection.other": "{count} Pl\xE4tze verf\xFCgbar",
148
+ "picker.overview": "\xDCbersicht",
149
+ "picker.tapSeatHint": "Tippen Sie auf einen Platz, um die Ansicht zu pr\xFCfen",
150
+ "picker.chartSize": "Kartengr\xF6\xDFe",
151
+ "picker.custom": "Benutzerdefiniert",
152
+ "picker.categories": "Kategorien",
153
+ "picker.accessibility": "Barrierefreiheit",
154
+ "picker.showAnyAccessible": "Jeden barrierefreien Platz anzeigen",
155
+ "picker.any": "Beliebig",
156
+ "picker.yourSeats": "Ihre Pl\xE4tze",
157
+ "picker.emptySeats": "Tippen Sie auf die Pl\xE4tze auf der Karte",
158
+ "picker.emptySeatsWithGa": "Tippen Sie auf die Pl\xE4tze auf der Karte \xB7 tippen Sie auf einen Stehbereich f\xFCr Stehplatztickets",
159
+ "picker.oneFewer": "Einen weniger",
160
+ "picker.oneMore": "Einen mehr",
161
+ "picker.remove": "{label} entfernen",
162
+ "picker.ticketTierFor": "Ticketklasse f\xFCr {label}",
163
+ "picker.total": "Gesamt: {amount}",
164
+ "picker.viewFromSeat": "Ansicht von Platz {label}",
165
+ "picker.real360": "REAL 360\xB0",
166
+ "picker.preview": "VORSCHAU",
167
+ "picker.open360": "\xD6ffnen Sie die 360\xB0-Ansicht",
168
+ "picker.sightline": "\u2248 {rows} Reihen von der B\xFChne \xB7 {angle}\xB0 seitlich versetzt",
169
+ "picker.bookedDemo": "Gebucht! (Demo)",
170
+ "picker.bookButton.one": "{count} Ticket buchen \u2014 {amount}",
171
+ "picker.bookButton.other": "{count} Tickets buchen \u2014 {amount}",
172
+ "picker.simulateCrowd": "Menschenmenge simulieren: {state}",
173
+ "picker.on": "AN",
174
+ "picker.off": "AUS",
175
+ "picker.panorama360": "360\xB0-Veranstaltungsfotos",
176
+ "picker.illustrationCaption": "Illustration \xB7 \u2248 {m} m von der B\xFChne"
177
+ };
178
+ }
179
+ });
180
+
181
+ // src/i18n/locales/fr.ts
182
+ var fr_exports = {};
183
+ __export(fr_exports, {
184
+ fr: () => fr
185
+ });
186
+ var fr;
187
+ var init_fr = __esm({
188
+ "src/i18n/locales/fr.ts"() {
189
+ "use strict";
190
+ fr = {
191
+ // shared
192
+ "common.cancel": "Annuler",
193
+ "common.close": "Fermer",
194
+ "common.done": "Termin\xE9",
195
+ "common.copied": "\u2713 Copi\xE9",
196
+ // buyer picker (extraction in progress — P4 of the port plan)
197
+ "picker.holdSeats": "R\xE9server les si\xE8ges et valider",
198
+ "picker.completeBooking": "Terminer la r\xE9servation",
199
+ "picker.seatsHeld": "Si\xE8ges r\xE9serv\xE9s \u2014 {time}",
200
+ "picker.holdExpired": "Votre r\xE9servation a expir\xE9 \u2014 les si\xE8ges ont \xE9t\xE9 lib\xE9r\xE9s. S\xE9lectionnez \xE0 nouveau.",
201
+ "picker.seatTaken": "Le si\xE8ge {label} vient d'\xEAtre r\xE9serv\xE9 par un autre acheteur.",
202
+ "picker.poweredBy": "Propuls\xE9 par SeatLayer",
203
+ "picker.colorblind": "Couleurs adapt\xE9es au daltonisme",
204
+ "picker.orphanHint": "Cela laisse un si\xE8ge isol\xE9 \u2014 pensez \xE0 vous d\xE9caler d'un si\xE8ge.",
205
+ "picker.seats.one": "{count} si\xE8ge",
206
+ "picker.seats.other": "{count} si\xE8ges",
207
+ // renderer (drawn on the Konva map — shared by the embed SDK)
208
+ "map.aria": "Plan de salle. Utilisez les fl\xE8ches pour naviguer entre les si\xE8ges, Entr\xE9e pour s\xE9lectionner.",
209
+ "map.seatsLeft": "{count} LIBRES",
210
+ "map.fromPrice": "D\xC8S {price}",
211
+ // buyer picker page (src/pages/PickerPage.tsx)
212
+ "picker.language": "Langue",
213
+ "picker.zoomToFit": "Ajuster le zoom",
214
+ "picker.viewMode": "Mode d'affichage",
215
+ "picker.floor": "\xC9tage",
216
+ "picker.zoomLevel": "Niveau de zoom",
217
+ "picker.rungTip.zones": "Vue la plus large \u2014 blocs de zones (\xC9tage / Parquet inf\xE9rieur / Parquet sup\xE9rieur)",
218
+ "picker.rungTip.sections": "Blocs de sections \u2014 m\xE9lange de cat\xE9gories + teinte de disponibilit\xE9",
219
+ "picker.rungTip.seats": "Si\xE8ges individuels \u2014 les blocs se transforment en points",
220
+ "picker.rungLabel.zones": "ZONES",
221
+ "picker.rungLabel.sections": "SECTIONS",
222
+ "picker.rungLabel.seats": "SI\xC8GES",
223
+ "picker.sectionSummaryAria": "R\xE9sum\xE9 de la section {label}",
224
+ "picker.closeSectionSummary": "Fermer le r\xE9sum\xE9 de la section",
225
+ "picker.seatsLeftInSection.one": "{count} si\xE8ge restant",
226
+ "picker.seatsLeftInSection.other": "{count} si\xE8ges restants",
227
+ "picker.overview": "Vue d'ensemble",
228
+ "picker.tapSeatHint": "Appuyez sur un si\xE8ge pour v\xE9rifier sa vue",
229
+ "picker.chartSize": "Taille du plan",
230
+ "picker.custom": "Personnalis\xE9",
231
+ "picker.categories": "Cat\xE9gories",
232
+ "picker.accessibility": "Accessibilit\xE9",
233
+ "picker.showAnyAccessible": "Afficher un si\xE8ge accessible",
234
+ "picker.any": "N'importe quel",
235
+ "picker.yourSeats": "Vos si\xE8ges",
236
+ "picker.emptySeats": "Appuyez sur les si\xE8ges sur le plan",
237
+ "picker.emptySeatsWithGa": "Appuyez sur les si\xE8ges sur le plan \xB7 appuyez sur une zone debout pour les billets GA",
238
+ "picker.oneFewer": "Un de moins",
239
+ "picker.oneMore": "Un de plus",
240
+ "picker.remove": "Supprimer {label}",
241
+ "picker.ticketTierFor": "Cat\xE9gorie de billet pour {label}",
242
+ "picker.total": "Total : {amount}",
243
+ "picker.viewFromSeat": "Vue depuis le si\xE8ge {label}",
244
+ "picker.real360": "VRAI 360\xB0",
245
+ "picker.preview": "APER\xC7U",
246
+ "picker.open360": "Ouvrir la vue \xE0 360\xB0",
247
+ "picker.sightline": "\u2248 {rows} rang\xE9es de la sc\xE8ne \xB7 {angle}\xB0 d\xE9cal\xE9 du centre",
248
+ "picker.bookedDemo": "R\xE9serv\xE9 ! (d\xE9mo)",
249
+ "picker.bookButton.one": "R\xE9server {count} billet \u2014 {amount}",
250
+ "picker.bookButton.other": "R\xE9server {count} billets \u2014 {amount}",
251
+ "picker.simulateCrowd": "Simuler la foule : {state}",
252
+ "picker.on": "ACTIV\xC9",
253
+ "picker.off": "D\xC9SACTIV\xC9",
254
+ "picker.panorama360": "Photo panoramique 360\xB0 du lieu",
255
+ "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m de la sc\xE8ne"
256
+ };
257
+ }
258
+ });
259
+
20
260
  // src/index.ts
21
261
  var index_exports = {};
22
262
  __export(index_exports, {
23
263
  ACCESSIBILITY_TYPES: () => ACCESSIBILITY_TYPES,
24
264
  CHART_STORAGE_KEY: () => CHART_STORAGE_KEY,
265
+ DEFAULT_CURRENCY: () => DEFAULT_CURRENCY,
25
266
  PickerController: () => PickerController,
267
+ SUPPORTED_LOCALES: () => SUPPORTED_LOCALES,
26
268
  SeatmapRenderer: () => SeatmapRenderer,
269
+ UNGROUPED_ID: () => UNGROUPED_ID,
27
270
  accessibilityMeta: () => accessibilityMeta,
271
+ allObjects: () => allObjects,
272
+ applyHidden: () => applyHidden,
28
273
  chartBounds: () => chartBounds,
274
+ computeSections: () => computeSections,
29
275
  createRenderer: () => createRenderer,
276
+ currencySymbol: () => currencySymbol,
30
277
  expandBooth: () => expandBooth,
31
278
  expandChart: () => expandChart,
32
279
  expandRow: () => expandRow,
33
280
  expandRowSlots: () => expandRowSlots,
34
281
  expandTable: () => expandTable,
282
+ floorObjects: () => floorObjects,
283
+ floorsOf: () => floorsOf,
284
+ formatDate: () => formatDate,
285
+ formatMoney: () => formatMoney,
286
+ getLocale: () => getLocale,
287
+ hiddenObjectIds: () => hiddenObjectIds,
288
+ isSectionHidden: () => isSectionHidden,
35
289
  layerOf: () => layerOf,
290
+ loadLocale: () => loadLocale,
36
291
  objectCenter: () => objectCenter,
37
- pointInPolygon: () => pointInPolygon
292
+ pointInPolygon: () => pointInPolygon,
293
+ resolveLocale: () => resolveLocale,
294
+ setLocale: () => setLocale,
295
+ setMoneyLocale: () => setMoneyLocale,
296
+ setStringOverrides: () => setStringOverrides,
297
+ stackFloors: () => stackFloors,
298
+ t: () => t,
299
+ tCount: () => tCount
38
300
  });
39
301
  module.exports = __toCommonJS(index_exports);
40
302
 
@@ -118,7 +380,15 @@ function expandRowSlots(row) {
118
380
  const start = row.seatLabelStart ?? 1;
119
381
  const dir = row.seatNumbering?.direction ?? "ltr";
120
382
  const step = row.seatNumbering?.step ?? 1;
121
- const seatNumber = (i) => start + (dir === "rtl" ? row.seatCount - 1 - i : i) * step;
383
+ const n = row.seatCount;
384
+ let seatNumber;
385
+ if (dir === "center") {
386
+ const rank = new Array(n);
387
+ 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);
388
+ seatNumber = (i) => start + rank[i] * step;
389
+ } else {
390
+ seatNumber = (i) => start + (dir === "rtl" ? n - 1 - i : i) * step;
391
+ }
122
392
  const ov = overrideMap(row);
123
393
  return rowSeatPositions(row).map((p, i) => {
124
394
  const o = ov.get(i);
@@ -153,30 +423,41 @@ function expandRow(row) {
153
423
  }
154
424
  return seats;
155
425
  }
156
- function expandTable(t) {
426
+ function expandTable(t2) {
157
427
  const seats = [];
158
- const n = Math.max(0, Math.round(t.seatCount));
428
+ const n = Math.max(0, Math.round(t2.seatCount));
159
429
  if (n === 0) return seats;
160
430
  const mk = (i, x, y) => ({
161
- id: `${t.id}:${i}`,
162
- label: `${t.label}-${i + 1}`,
431
+ id: `${t2.id}:${i}`,
432
+ label: `${t2.label}-${i + 1}`,
163
433
  x,
164
434
  y,
165
- rowId: t.id,
166
- categoryKey: t.categoryKey
435
+ rowId: t2.id,
436
+ categoryKey: t2.categoryKey
167
437
  });
168
- if (t.shape === "round") {
169
- const R = (t.radius ?? 40) + TABLE_SEAT_OFFSET;
170
- const base = t.rotation * DEG;
438
+ if (t2.shape === "round") {
439
+ const R = (t2.radius ?? 40) + TABLE_SEAT_OFFSET;
440
+ const base = t2.rotation * DEG;
441
+ const arc = Math.max(0, Math.min(360, t2.seatArc ?? 360));
442
+ if (arc >= 360 || n === 1) {
443
+ for (let i = 0; i < n; i++) {
444
+ const a = base + i / n * 2 * Math.PI;
445
+ seats.push(mk(i, t2.center.x + R * Math.cos(a), t2.center.y + R * Math.sin(a)));
446
+ }
447
+ return seats;
448
+ }
449
+ const arcRad = arc * DEG;
450
+ const halfGap = (2 * Math.PI - arcRad) / 2;
451
+ const start = base + halfGap;
171
452
  for (let i = 0; i < n; i++) {
172
- const a = base + i / n * 2 * Math.PI;
173
- seats.push(mk(i, t.center.x + R * Math.cos(a), t.center.y + R * Math.sin(a)));
453
+ const a = start + i / (n - 1) * arcRad;
454
+ seats.push(mk(i, t2.center.x + R * Math.cos(a), t2.center.y + R * Math.sin(a)));
174
455
  }
175
456
  return seats;
176
457
  }
177
- const w = t.width ?? 80;
178
- const h = t.height ?? 50;
179
- const enabled = t.sides && t.sides.length ? t.sides : ["top", "bottom"];
458
+ const w = t2.width ?? 80;
459
+ const h = t2.height ?? 50;
460
+ const enabled = t2.sides && t2.sides.length ? t2.sides : ["top", "bottom"];
180
461
  const order = ["top", "bottom", "left", "right"].filter((s) => enabled.includes(s));
181
462
  if (!order.length) return seats;
182
463
  const counts = new Map(order.map((s) => [s, 0]));
@@ -203,7 +484,7 @@ function expandTable(t) {
203
484
  localX = w / 2 + TABLE_SEAT_OFFSET;
204
485
  localY = -h / 2 + (j + 0.5) * h / count;
205
486
  }
206
- const p = place(localX, localY, t.rotation, t.center);
487
+ const p = place(localX, localY, t2.rotation, t2.center);
207
488
  seats.push(mk(idx++, p.x, p.y));
208
489
  }
209
490
  }
@@ -274,9 +555,53 @@ function objectCenter(o) {
274
555
  return { x: o.x ?? 0, y: o.y ?? 0 };
275
556
  }
276
557
  }
558
+ function floorsOf(doc) {
559
+ if (doc.floors && doc.floors.length) return doc.floors;
560
+ return [{ id: "floor-0", name: "Main", objects: doc.objects, focalPoint: doc.focalPoint, backgroundImage: doc.backgroundImage }];
561
+ }
562
+ function floorObjects(doc, floorId) {
563
+ const floors = floorsOf(doc);
564
+ return (floorId ? floors.find((f) => f.id === floorId) : floors[0])?.objects ?? [];
565
+ }
566
+ function allObjects(doc) {
567
+ return doc.floors && doc.floors.length ? doc.floors.flatMap((f) => f.objects) : doc.objects;
568
+ }
569
+ function translateObject(o, dx, dy) {
570
+ const p = (pt) => ({ x: pt.x + dx, y: pt.y + dy });
571
+ const pts = (a) => a.map(p);
572
+ switch (o.type) {
573
+ case "row":
574
+ return { ...o, origin: p(o.origin) };
575
+ case "table":
576
+ case "booth":
577
+ return { ...o, center: p(o.center) };
578
+ case "gaArea":
579
+ return { ...o, points: pts(o.points) };
580
+ case "section":
581
+ return { ...o, outline: pts(o.outline) };
582
+ case "text":
583
+ return { ...o, position: p(o.position) };
584
+ case "shape":
585
+ return {
586
+ ...o,
587
+ ...o.points ? { points: pts(o.points) } : {},
588
+ ...o.x != null ? { x: o.x + dx } : {},
589
+ ...o.y != null ? { y: o.y + dy } : {}
590
+ };
591
+ }
592
+ }
593
+ function stackFloors(doc, spread = 900) {
594
+ if (!doc.floors || doc.floors.length < 2) return doc;
595
+ const objects = [];
596
+ doc.floors.forEach((f, i) => {
597
+ const dy = -i * spread;
598
+ for (const o of f.objects) objects.push(dy === 0 ? o : translateObject(o, 0, dy));
599
+ });
600
+ return { ...doc, objects, floors: void 0 };
601
+ }
277
602
  function expandChart(doc) {
278
603
  const out = [];
279
- for (const obj of doc.objects) {
604
+ for (const obj of allObjects(doc)) {
280
605
  if (obj.type === "row") out.push(...expandRow(obj));
281
606
  else if (obj.type === "table") out.push(...expandTable(obj));
282
607
  else if (obj.type === "booth") out.push(...expandBooth(obj));
@@ -296,7 +621,7 @@ function chartBounds(doc) {
296
621
  if (y > maxY) maxY = y;
297
622
  };
298
623
  for (const s of expandChart(doc)) acc(s.x, s.y);
299
- for (const obj of doc.objects) {
624
+ for (const obj of allObjects(doc)) {
300
625
  if (obj.type === "gaArea") {
301
626
  for (const p of obj.points) acc(p.x, p.y);
302
627
  } else if (obj.type === "section") {
@@ -341,6 +666,87 @@ function chartBounds(doc) {
341
666
  };
342
667
  }
343
668
 
669
+ // src/core/sections.ts
670
+ var UNGROUPED_ID = "__ungrouped__";
671
+ function objectSeatLabels(o) {
672
+ if (o.type === "row") return expandRow(o).map((s) => s.label);
673
+ if (o.type === "table") return expandTable(o).map((s) => s.label);
674
+ if (o.type === "booth") return expandBooth(o).map((s) => s.label);
675
+ return [];
676
+ }
677
+ function isSeatObject(o) {
678
+ return o.type === "row" || o.type === "table" || o.type === "booth";
679
+ }
680
+ function computeSections(doc) {
681
+ const objs = allObjects(doc);
682
+ const sectionObjs = objs.filter((o) => o.type === "section");
683
+ const nodes = /* @__PURE__ */ new Map();
684
+ for (const s of sectionObjs) {
685
+ nodes.set(s.id, { id: s.id, label: s.label || "Section", zone: s.zone, seatCount: 0, objectIds: [], seatLabels: [] });
686
+ }
687
+ const ungrouped = { id: UNGROUPED_ID, label: "Other seats", seatCount: 0, objectIds: [], seatLabels: [] };
688
+ const objectToSection = /* @__PURE__ */ new Map();
689
+ for (const obj of objs) {
690
+ if (!isSeatObject(obj)) continue;
691
+ const labels = objectSeatLabels(obj);
692
+ if (labels.length === 0) continue;
693
+ const c = objectCenter(obj);
694
+ const owner = sectionObjs.find((s) => pointInPolygon(c, s.outline));
695
+ const node = owner ? nodes.get(owner.id) : ungrouped;
696
+ node.seatCount += labels.length;
697
+ node.objectIds.push(obj.id);
698
+ node.seatLabels.push(...labels);
699
+ objectToSection.set(obj.id, node.id);
700
+ }
701
+ return {
702
+ sections: sectionObjs.map((s) => nodes.get(s.id)),
703
+ ungrouped: ungrouped.objectIds.length ? ungrouped : null,
704
+ objectToSection
705
+ };
706
+ }
707
+ function isSectionHidden(s, hidden) {
708
+ return hidden.has(s.id) || !!s.zone && hidden.has(s.zone);
709
+ }
710
+ function hiddenObjectIds(doc, hidden) {
711
+ const out = /* @__PURE__ */ new Set();
712
+ if (!hidden.size) return out;
713
+ const { sections, ungrouped, objectToSection } = computeSections(doc);
714
+ const hiddenSectionIds = /* @__PURE__ */ new Set();
715
+ const zoneById = new Map(sections.map((s) => [s.id, s.zone]));
716
+ for (const s of sections) {
717
+ const zone = zoneById.get(s.id);
718
+ if (hidden.has(s.id) || zone && hidden.has(zone)) hiddenSectionIds.add(s.id);
719
+ }
720
+ const ungroupedHidden = !!ungrouped && hidden.has(UNGROUPED_ID);
721
+ for (const [objId, secId] of objectToSection) {
722
+ if (hiddenSectionIds.has(secId) || ungroupedHidden && secId === UNGROUPED_ID) out.add(objId);
723
+ }
724
+ return out;
725
+ }
726
+ function applyHidden(doc, hidden) {
727
+ if (!hidden.size) return doc;
728
+ const hide = hiddenObjectIds(doc, hidden);
729
+ const hiddenSet = hidden instanceof Set ? hidden : new Set(hidden);
730
+ const keep = (o) => {
731
+ if (hide.has(o.id)) return false;
732
+ if (o.type === "section" && isSectionHidden(o, hiddenSet)) return false;
733
+ return true;
734
+ };
735
+ if (doc.floors && doc.floors.length) {
736
+ let changed = false;
737
+ const floors = doc.floors.map((f) => {
738
+ const objects2 = f.objects.filter(keep);
739
+ if (objects2.length !== f.objects.length) changed = true;
740
+ return objects2.length === f.objects.length ? f : { ...f, objects: objects2 };
741
+ });
742
+ if (!changed) return doc;
743
+ return { ...doc, floors, objects: floors[0].objects };
744
+ }
745
+ const objects = doc.objects.filter(keep);
746
+ if (objects.length === doc.objects.length) return doc;
747
+ return { ...doc, objects };
748
+ }
749
+
344
750
  // src/engine/SeatmapRenderer.ts
345
751
  var import_Core = require("konva/lib/Core");
346
752
  var import_Stage = require("konva/lib/Stage");
@@ -352,26 +758,170 @@ var import_Ellipse = require("konva/lib/shapes/Ellipse");
352
758
  var import_Line = require("konva/lib/shapes/Line");
353
759
  var import_Text = require("konva/lib/shapes/Text");
354
760
  var import_Image = require("konva/lib/shapes/Image");
761
+
762
+ // src/lib/money.ts
763
+ var DEFAULT_CURRENCY = "USD";
764
+ var displayLocale;
765
+ function setMoneyLocale(locale) {
766
+ displayLocale = locale;
767
+ }
768
+ var formatterCache = /* @__PURE__ */ new Map();
769
+ function formatter(currency, digits) {
770
+ const key = `${displayLocale ?? ""}|${currency}|${digits ?? "auto"}`;
771
+ let fmt = formatterCache.get(key);
772
+ if (!fmt) {
773
+ fmt = new Intl.NumberFormat(displayLocale, {
774
+ style: "currency",
775
+ currency,
776
+ minimumFractionDigits: digits,
777
+ maximumFractionDigits: digits
778
+ });
779
+ formatterCache.set(key, fmt);
780
+ }
781
+ return fmt;
782
+ }
783
+ function formatMoney(amount, currency = DEFAULT_CURRENCY, fractionDigits) {
784
+ const digits = fractionDigits ?? (Number.isInteger(amount) ? 0 : void 0);
785
+ return formatter(currency, digits).format(amount);
786
+ }
787
+ function currencySymbol(currency = DEFAULT_CURRENCY) {
788
+ const part = formatter(currency, 0).formatToParts(0).find((p) => p.type === "currency");
789
+ return part?.value ?? currency;
790
+ }
791
+
792
+ // src/i18n/locales/en.ts
793
+ var en = {
794
+ // shared
795
+ "common.cancel": "Cancel",
796
+ "common.close": "Close",
797
+ "common.done": "Done",
798
+ "common.copied": "\u2713 Copied",
799
+ // buyer picker (extraction in progress — P4 of the port plan)
800
+ "picker.holdSeats": "Hold seats & checkout",
801
+ "picker.completeBooking": "Complete booking",
802
+ "picker.seatsHeld": "Seats held \u2014 {time}",
803
+ "picker.holdExpired": "Your hold expired \u2014 the seats were released. Pick again.",
804
+ "picker.seatTaken": "Seat {label} was just taken by another buyer.",
805
+ "picker.poweredBy": "Powered by SeatLayer",
806
+ "picker.colorblind": "Colorblind-friendly colors",
807
+ "picker.orphanHint": "This leaves a single seat stranded \u2014 consider shifting one seat over.",
808
+ "picker.seats.one": "{count} seat",
809
+ "picker.seats.other": "{count} seats",
810
+ // renderer (drawn on the Konva map — shared by the embed SDK)
811
+ "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
812
+ "map.seatsLeft": "{count} LEFT",
813
+ "map.fromPrice": "FROM {price}",
814
+ // buyer picker page (src/pages/PickerPage.tsx)
815
+ "picker.language": "Language",
816
+ "picker.zoomToFit": "Zoom to fit",
817
+ "picker.viewMode": "View mode",
818
+ "picker.floor": "Floor",
819
+ "picker.zoomLevel": "Zoom level",
820
+ "picker.rungTip.zones": "Farthest rung \u2014 zone blocks (Floor / Lower Bowl / Upper Bowl)",
821
+ "picker.rungTip.sections": "Section blocks \u2014 category-mix fill + availability tint",
822
+ "picker.rungTip.seats": "Individual seats \u2014 blocks melt into dots",
823
+ "picker.rungLabel.zones": "ZONES",
824
+ "picker.rungLabel.sections": "SECTIONS",
825
+ "picker.rungLabel.seats": "SEATS",
826
+ "picker.sectionSummaryAria": "{label} section summary",
827
+ "picker.closeSectionSummary": "Close section summary",
828
+ "picker.seatsLeftInSection.one": "{count} seat left",
829
+ "picker.seatsLeftInSection.other": "{count} seats left",
830
+ "picker.overview": "Overview",
831
+ "picker.tapSeatHint": "Tap any seat to check its view",
832
+ "picker.chartSize": "Chart size",
833
+ "picker.custom": "Custom",
834
+ "picker.categories": "Categories",
835
+ "picker.accessibility": "Accessibility",
836
+ "picker.showAnyAccessible": "Show any accessible seat",
837
+ "picker.any": "Any",
838
+ "picker.yourSeats": "Your seats",
839
+ "picker.emptySeats": "Tap seats on the map",
840
+ "picker.emptySeatsWithGa": "Tap seats on the map \xB7 tap a standing area for GA tickets",
841
+ "picker.oneFewer": "One fewer",
842
+ "picker.oneMore": "One more",
843
+ "picker.remove": "Remove {label}",
844
+ "picker.ticketTierFor": "Ticket tier for {label}",
845
+ "picker.total": "Total: {amount}",
846
+ "picker.viewFromSeat": "View from seat {label}",
847
+ "picker.real360": "REAL 360\xB0",
848
+ "picker.preview": "PREVIEW",
849
+ "picker.open360": "Open 360\xB0 view",
850
+ "picker.sightline": "\u2248 {rows} rows from stage \xB7 {angle}\xB0 off-center",
851
+ "picker.bookedDemo": "Booked! (demo)",
852
+ "picker.bookButton.one": "Book {count} ticket \u2014 {amount}",
853
+ "picker.bookButton.other": "Book {count} tickets \u2014 {amount}",
854
+ "picker.simulateCrowd": "Simulate crowd: {state}",
855
+ "picker.on": "ON",
856
+ "picker.off": "OFF",
857
+ "picker.panorama360": "360\xB0 venue photo",
858
+ "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m from stage"
859
+ };
860
+
861
+ // src/i18n/index.ts
862
+ var SUPPORTED_LOCALES = ["en", "es", "de", "fr"];
863
+ var bundles = { en };
864
+ var overrides = {};
865
+ var active = "en";
866
+ function resolveLocale(explicit, stored) {
867
+ for (const candidate of [explicit, stored, typeof navigator !== "undefined" ? navigator.language : null]) {
868
+ if (!candidate) continue;
869
+ const base = candidate.toLowerCase().split("-")[0];
870
+ if (SUPPORTED_LOCALES.includes(base)) return base;
871
+ }
872
+ return "en";
873
+ }
874
+ function getLocale() {
875
+ return active;
876
+ }
877
+ function setLocale(locale, bundle) {
878
+ if (bundle) bundles[locale] = { ...bundles[locale], ...bundle };
879
+ active = bundles[locale] ? locale : "en";
880
+ setMoneyLocale(active);
881
+ if (typeof document !== "undefined") document.documentElement.lang = active;
882
+ }
883
+ function setStringOverrides(next) {
884
+ overrides = next;
885
+ }
886
+ var PLACEHOLDER = /\{(\w+)\}/g;
887
+ function t(key, vars) {
888
+ const raw = overrides[key] ?? bundles[active]?.[key] ?? en[key] ?? key;
889
+ if (!vars) return raw;
890
+ return raw.replace(PLACEHOLDER, (_, name) => String(vars[name] ?? `{${name}}`));
891
+ }
892
+ function tCount(key, count, vars) {
893
+ const rule = new Intl.PluralRules(active).select(count);
894
+ const exact = overrides[`${key}.${rule}`] ?? bundles[active]?.[`${key}.${rule}`] ?? en[`${key}.${rule}`];
895
+ const raw = exact ?? overrides[`${key}.other`] ?? bundles[active]?.[`${key}.other`] ?? en[`${key}.other`] ?? key;
896
+ return raw.replace(PLACEHOLDER, (_, name) => String({ count, ...vars }[name] ?? `{${name}}`));
897
+ }
898
+ function formatDate(value, opts) {
899
+ return new Intl.DateTimeFormat(active, opts ?? { dateStyle: "medium", timeStyle: "short" }).format(value);
900
+ }
901
+
902
+ // src/engine/SeatmapRenderer.ts
355
903
  var SEAT_RADIUS = 9;
356
904
  var SEAT_LEGIBLE_SCALE = 0.9;
357
905
  var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
358
906
  var LABEL_SCALE = 1;
359
- var SECTION_PROMINENT_SCALE = 0.35 * SEAT_LEGIBLE_SCALE;
907
+ var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
908
+ var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
360
909
  var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
361
910
  var MAX_LABELS = 700;
362
911
  var ISO_ANGLE_DEG = -11.5;
363
912
  var ISO_SQUASH = 0.58;
364
- var LIFT_PER_STEP = 46;
913
+ var LIFT_PER_STEP = 58;
365
914
  var ISO_TWEEN_MS = 320;
366
915
  var BLOCK_FILL_ALPHA = 0.85;
367
916
  var SOLD_DARKEN = 0.5;
368
- var SECTION_LABEL_PX = 18;
369
- var SECTION_SUB_PX = 11;
917
+ var SECTION_LABEL_PX = 20;
918
+ var SECTION_SUB_PX = 12.5;
370
919
  var ZONE_LABEL_PX = 30;
371
920
  var ZONE_SUB_PX = 12;
372
921
  var HELD_FILL = "#6b7280";
373
922
  var TAKEN_FILL = "#374151";
374
923
  var NFS_STROKE = "#4b5563";
924
+ var CB_PALETTE = ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"];
375
925
  var ACCESS_RING = {
376
926
  wheelchair: "#3b82f6",
377
927
  companion: "#8b5cf6",
@@ -383,7 +933,7 @@ var ACCESS_RING = {
383
933
  };
384
934
  function seatMatchesAccess(seat, filter) {
385
935
  if (filter.length === 0) return !!seat.accessible;
386
- return !!seat.accessibility?.some((t) => filter.includes(t));
936
+ return !!seat.accessibility?.some((t2) => filter.includes(t2));
387
937
  }
388
938
  var DEF_SEAT_LABEL = "#0b1220";
389
939
  var DEF_SELECTION = "#ffffff";
@@ -452,16 +1002,21 @@ function mixColors(parts, fallback) {
452
1002
  }
453
1003
  return tw > 0 ? toHex(r / tw, g / tw, b / tw) : fallback;
454
1004
  }
455
- function lerpColor(a, b, t) {
1005
+ function lerpColor(a, b, t2) {
456
1006
  const ca = hexToRgb(a);
457
1007
  const cb = hexToRgb(b);
458
1008
  if (!ca || !cb) return a;
459
- return toHex(ca[0] + (cb[0] - ca[0]) * t, ca[1] + (cb[1] - ca[1]) * t, ca[2] + (cb[2] - ca[2]) * t);
1009
+ return toHex(ca[0] + (cb[0] - ca[0]) * t2, ca[1] + (cb[1] - ca[1]) * t2, ca[2] + (cb[2] - ca[2]) * t2);
460
1010
  }
461
1011
  var _SeatmapRenderer = class _SeatmapRenderer {
462
1012
  constructor(container, options = {}) {
463
1013
  this.seats = [];
464
1014
  this.seatById = /* @__PURE__ */ new Map();
1015
+ /** Multi-floor (Batch 5): the last-set chart + which floor we're rendering. */
1016
+ this.chartDoc = null;
1017
+ this.activeFloorId = "";
1018
+ /** When true on a multi-floor chart, render ALL floors stacked (3D overview). */
1019
+ this.stacked = false;
465
1020
  /** Interactive node per seat/booth — a Circle for seats, a Rect for booths. */
466
1021
  this.circleById = /* @__PURE__ */ new Map();
467
1022
  /** Booth block geometry, keyed by booth id (= the unit's rowId). */
@@ -469,6 +1024,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
469
1024
  this.statusById = /* @__PURE__ */ new Map();
470
1025
  this.catColor = /* @__PURE__ */ new Map();
471
1026
  this.theme = {};
1027
+ /** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
1028
+ this.colorblind = false;
1029
+ /** Category order from the doc — the stable index into the CB palette. */
1030
+ this.catOrder = [];
472
1031
  this.selection = /* @__PURE__ */ new Set();
473
1032
  this.selectionRings = /* @__PURE__ */ new Map();
474
1033
  this.focusedId = null;
@@ -485,6 +1044,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
485
1044
  this.zones = [];
486
1045
  this.seatSection = /* @__PURE__ */ new Map();
487
1046
  this.catPrice = /* @__PURE__ */ new Map();
1047
+ /** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
1048
+ this.dimmedSections = /* @__PURE__ */ new Set();
1049
+ /** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
1050
+ this.objectFloor = /* @__PURE__ */ new Map();
488
1051
  /** Zone id → colour (drives extruded side faces in iso view). */
489
1052
  this.zoneColor = /* @__PURE__ */ new Map();
490
1053
  this.hasSections = false;
@@ -497,6 +1060,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
497
1060
  this.isoRaf = 0;
498
1061
  /** Chart centre the iso projection pivots about (bounds centre). */
499
1062
  this.isoCentre = { x: 0, y: 0 };
1063
+ /** rAF for an in-flight camera glide (focusRegion / setRung); 0 = none. */
1064
+ this.glideRaf = 0;
500
1065
  /** Set in destroy() so an in-flight iso tween bails. */
501
1066
  this.destroyed = false;
502
1067
  /** Cached scale the section/zone labels were last sized for (scale-compensation). */
@@ -544,6 +1109,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
544
1109
  // pointer events away from Konva's canvas, killing its click/tap/hover
545
1110
  // pipeline. We rely on bubbling instead.
546
1111
  this.onPointerDown = (e) => {
1112
+ this.cancelGlide();
547
1113
  this.pointers.set(e.pointerId, this.toLocal(e));
548
1114
  if (this.pointers.size === 1) {
549
1115
  this.moved = 0;
@@ -605,6 +1171,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
605
1171
  };
606
1172
  this.container = container;
607
1173
  this.opts = { maxSelection: 10, selectableStatuses: ["free"], ...options };
1174
+ this.currency = options.currency;
608
1175
  import_Core.Konva.pixelRatio = this.dpr;
609
1176
  this.stage = new import_Stage.Stage({
610
1177
  container,
@@ -621,7 +1188,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
621
1188
  if (container.tabIndex < 0) container.tabIndex = 0;
622
1189
  container.setAttribute("role", "application");
623
1190
  if (!container.getAttribute("aria-label")) {
624
- container.setAttribute("aria-label", "Seating map. Use arrow keys to move between seats, Enter to select.");
1191
+ container.setAttribute("aria-label", t("map.aria"));
625
1192
  }
626
1193
  container.addEventListener("keydown", this.onKeyDown);
627
1194
  this.bgLayer = new import_Layer.Layer({ listening: true });
@@ -664,7 +1231,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
664
1231
  }
665
1232
  }
666
1233
  // ---- ISeatmapRenderer -----------------------------------------------------
667
- setChart(doc) {
1234
+ setChart(doc, opts) {
1235
+ if (doc !== this.chartDoc) this.stacked = false;
1236
+ this.chartDoc = doc;
1237
+ this.activeFloorId = opts?.floorId ?? floorsOf(doc)[0].id;
1238
+ this.objectFloor.clear();
1239
+ if (doc.floors && doc.floors.length > 1) {
1240
+ for (const f of doc.floors) for (const o of f.objects) this.objectFloor.set(o.id, f.id);
1241
+ }
1242
+ const view = this.floorView(doc);
668
1243
  this.focusedId = null;
669
1244
  this.focusRing.visible(false);
670
1245
  this.bgLayer.destroyChildren();
@@ -692,7 +1267,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
692
1267
  this.isoT = 0;
693
1268
  this.isoTarget = 0;
694
1269
  this.resetLayerTransforms();
695
- this.hasSections = doc.objects.some((o) => o.type === "section");
1270
+ this.hasSections = view.objects.some((o) => o.type === "section");
696
1271
  for (const z of doc.zones ?? []) if (z.color) this.zoneColor.set(z.id, z.color);
697
1272
  this.seatLayer.opacity(1);
698
1273
  this.hoverRing.visible(false);
@@ -703,28 +1278,61 @@ var _SeatmapRenderer = class _SeatmapRenderer {
703
1278
  this.hoverRing.radius(this.seatR + 2);
704
1279
  this.catColor.clear();
705
1280
  this.catPrice.clear();
1281
+ this.catOrder = doc.categories.map((c) => c.key);
706
1282
  for (const c of doc.categories) {
707
1283
  this.catColor.set(c.key, c.color);
708
1284
  if (typeof c.price === "number") this.catPrice.set(c.key, c.price);
709
1285
  }
710
- for (const obj of doc.objects) {
1286
+ for (const obj of view.objects) {
711
1287
  if (obj.type === "booth") {
712
1288
  this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
713
1289
  }
714
1290
  }
715
- this.seats = expandChart(doc);
1291
+ this.seats = expandChart(view);
716
1292
  for (const s of this.seats) {
717
1293
  this.seatById.set(s.id, s);
718
1294
  this.statusById.set(s.id, "free");
719
1295
  }
720
- this.renderBackground(doc);
1296
+ this.renderBackground(view);
721
1297
  this.renderSeats();
722
1298
  this.overlayLayer.add(this.labelGroup);
723
1299
  this.overlayLayer.add(this.hoverRing);
724
- this.bounds = chartBounds(doc);
1300
+ this.bounds = chartBounds(view);
725
1301
  this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
726
1302
  this.zoomToFit();
727
1303
  }
1304
+ /** The chart to render: all floors stacked (3D overview), the active floor, or
1305
+ * the whole chart for single-floor charts. */
1306
+ floorView(doc) {
1307
+ if (!doc.floors || !doc.floors.length) return doc;
1308
+ if (this.stacked && doc.floors.length >= 2) return stackFloors(doc);
1309
+ const floor = doc.floors.find((f) => f.id === this.activeFloorId) ?? doc.floors[0];
1310
+ return { ...doc, objects: floor.objects, focalPoint: floor.focalPoint, backgroundImage: floor.backgroundImage, floors: void 0 };
1311
+ }
1312
+ /** Toggle the 3D all-floors stacked overview (Batch 5). Re-renders; no-op on
1313
+ * single-floor charts. The caller re-applies statuses + animates the iso view. */
1314
+ setStacked(on) {
1315
+ if (!this.chartDoc || on === this.stacked) return;
1316
+ const multi = !!this.chartDoc.floors && this.chartDoc.floors.length >= 2;
1317
+ if (!multi) return;
1318
+ this.stacked = on;
1319
+ this.setChart(this.chartDoc, { floorId: this.activeFloorId });
1320
+ }
1321
+ isStacked() {
1322
+ return this.stacked;
1323
+ }
1324
+ /** Switch which floor is shown (2D). Re-renders + re-fits; no-op if unchanged. */
1325
+ setActiveFloor(floorId) {
1326
+ if (!this.chartDoc || floorId === this.activeFloorId) return;
1327
+ this.setChart(this.chartDoc, { floorId });
1328
+ }
1329
+ /** Floors for the host's switcher (single-floor charts return one synthetic floor). */
1330
+ getFloors() {
1331
+ return this.chartDoc ? floorsOf(this.chartDoc).map((f) => ({ id: f.id, name: f.name })) : [];
1332
+ }
1333
+ getActiveFloorId() {
1334
+ return this.activeFloorId;
1335
+ }
728
1336
  setStatus(seatIds, status) {
729
1337
  let touched = false;
730
1338
  const affected = this.hasSections ? /* @__PURE__ */ new Set() : null;
@@ -804,11 +1412,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
804
1412
  const dur = 620;
805
1413
  const step = (now) => {
806
1414
  if (!ring.getLayer()) return;
807
- const t = Math.min(1, (now - start) / dur);
808
- ring.radius(this.seatR * (1 + t * 1.8));
809
- ring.opacity(0.9 * (1 - t));
1415
+ const t2 = Math.min(1, (now - start) / dur);
1416
+ ring.radius(this.seatR * (1 + t2 * 1.8));
1417
+ ring.opacity(0.9 * (1 - t2));
810
1418
  this.overlayLayer.batchDraw();
811
- if (t < 1) requestAnimationFrame(step);
1419
+ if (t2 < 1) requestAnimationFrame(step);
812
1420
  else {
813
1421
  ring.destroy();
814
1422
  this.overlayLayer.batchDraw();
@@ -915,7 +1523,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
915
1523
  sameAccessFilter(next) {
916
1524
  const cur = this.accessFilter;
917
1525
  if (cur === null || next === null) return cur === next;
918
- return cur.length === next.length && cur.every((t, i) => t === next[i]);
1526
+ return cur.length === next.length && cur.every((t2, i) => t2 === next[i]);
919
1527
  }
920
1528
  // ---- isometric ("3D") view mode -------------------------------------------
921
1529
  /**
@@ -963,6 +1571,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
963
1571
  };
964
1572
  this.isoRaf = requestAnimationFrame(step);
965
1573
  }
1574
+ /** Current projection — reflects the tween target, not the mid-tween isoT. */
1575
+ getViewMode() {
1576
+ return this.isoTarget === 1 ? "isometric" : "flat";
1577
+ }
966
1578
  /** Iso angle (rad) + y-squash for the current isoT. */
967
1579
  isoParams() {
968
1580
  return { th: ISO_ANGLE_DEG * Math.PI / 180 * this.isoT, sg: 1 - (1 - ISO_SQUASH) * this.isoT };
@@ -1020,8 +1632,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1020
1632
  * lift + extruded side faces on elevated sections.
1021
1633
  */
1022
1634
  applyIso() {
1023
- const t = this.isoT;
1024
- if (t === 0) {
1635
+ const t2 = this.isoT;
1636
+ if (t2 === 0) {
1025
1637
  this.resetLayerTransforms();
1026
1638
  } else {
1027
1639
  const { th, sg } = this.isoParams();
@@ -1074,13 +1686,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1074
1686
  }
1075
1687
  /** Lift elevated sections (+ their members) and update their extruded side faces. */
1076
1688
  applyElevation() {
1077
- const t = this.isoT;
1689
+ const t2 = this.isoT;
1078
1690
  for (const sec of this.sections) {
1079
1691
  if (sec.elevation <= 0) continue;
1080
1692
  const off = this.isoLiftLocal(sec.elevation);
1081
1693
  sec.liftGroupBg?.position(off);
1082
1694
  sec.liftGroupSeat?.position(off);
1083
- const alpha = 0.9 * t;
1695
+ const alpha = 0.9 * t2;
1084
1696
  for (let i = 0; i < sec.sideFaces.length; i++) {
1085
1697
  const face = sec.sideFaces[i];
1086
1698
  const p0 = sec.outline[i];
@@ -1094,6 +1706,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1094
1706
  this.destroyed = true;
1095
1707
  if (this.rafId) cancelAnimationFrame(this.rafId);
1096
1708
  if (this.isoRaf) cancelAnimationFrame(this.isoRaf);
1709
+ if (this.glideRaf) cancelAnimationFrame(this.glideRaf);
1097
1710
  if (this.viewChangeRaf) cancelAnimationFrame(this.viewChangeRaf);
1098
1711
  if (this.recacheTimer) clearTimeout(this.recacheTimer);
1099
1712
  this.resizeObs?.disconnect();
@@ -1173,7 +1786,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1173
1786
  this.circleById.set(seat.id, rect);
1174
1787
  this.paintSeat(rect, seat.id);
1175
1788
  target.add(rect);
1176
- const t = new import_Text.Text({
1789
+ const t2 = new import_Text.Text({
1177
1790
  x: seat.x,
1178
1791
  y: seat.y,
1179
1792
  text: seat.label,
@@ -1184,17 +1797,23 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1184
1797
  listening: false,
1185
1798
  perfectDrawEnabled: false
1186
1799
  });
1187
- t.offsetX(t.width() / 2);
1188
- t.offsetY(t.height() / 2);
1800
+ t2.offsetX(t2.width() / 2);
1801
+ t2.offsetY(t2.height() / 2);
1189
1802
  this.hasBoothText = true;
1190
- target.add(t);
1803
+ target.add(t2);
1804
+ }
1805
+ /** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
1806
+ seatBaseColor(categoryKey) {
1807
+ if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
1808
+ const idx = this.catOrder.indexOf(categoryKey);
1809
+ return CB_PALETTE[(idx >= 0 ? idx : 0) % CB_PALETTE.length];
1191
1810
  }
1192
1811
  /** Apply fill/stroke/opacity for a seat's current status + selection. */
1193
1812
  paintSeat(c, id) {
1194
1813
  const seat = this.seatById.get(id);
1195
1814
  const status = this.statusById.get(id) ?? "free";
1196
1815
  const selected = this.selection.has(id);
1197
- const base = this.catColor.get(seat.categoryKey) ?? "#6e7bff";
1816
+ const base = this.seatBaseColor(seat.categoryKey);
1198
1817
  c.dash([]);
1199
1818
  c.strokeWidth(0);
1200
1819
  c.stroke("");
@@ -1207,8 +1826,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1207
1826
  c.fill(HELD_FILL);
1208
1827
  break;
1209
1828
  case "booked":
1210
- c.fill(TAKEN_FILL);
1211
- c.opacity(0.45);
1829
+ if (this.colorblind) {
1830
+ c.fill("rgba(0,0,0,0)");
1831
+ c.stroke(TAKEN_FILL);
1832
+ c.strokeWidth(1.5);
1833
+ c.opacity(0.9);
1834
+ } else {
1835
+ c.fill(TAKEN_FILL);
1836
+ c.opacity(0.45);
1837
+ }
1212
1838
  break;
1213
1839
  case "not_for_sale":
1214
1840
  c.fill(TAKEN_FILL);
@@ -1223,6 +1849,50 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1223
1849
  if (this.categoryHighlight && status === "free" && !selected && seat.categoryKey !== this.categoryHighlight) {
1224
1850
  c.opacity(0.25);
1225
1851
  }
1852
+ if (this.dimmedSections.size) {
1853
+ const sec = this.seatSection.get(id);
1854
+ if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
1855
+ c.opacity(0.18);
1856
+ }
1857
+ }
1858
+ }
1859
+ /**
1860
+ * Colorblind-safe mode: swap category hues for the Okabe-Ito palette and
1861
+ * render booked seats hollow. Off restores the exact default rendering.
1862
+ */
1863
+ setColorblindSafe(on) {
1864
+ if (on === this.colorblind) return;
1865
+ this.colorblind = on;
1866
+ for (const seat of this.seats) {
1867
+ const c = this.circleById.get(seat.id);
1868
+ if (c) this.paintSeat(c, seat.id);
1869
+ }
1870
+ if (this.cached) {
1871
+ this.seatLayer.clearCache();
1872
+ this.cacheSeatLayer();
1873
+ } else {
1874
+ this.seatLayer.batchDraw();
1875
+ }
1876
+ }
1877
+ /**
1878
+ * Dim the seats of these section/zone ids (organizer manager use) so held-back
1879
+ * inventory is visually distinct on the canvas without hiding it. `null`/empty
1880
+ * clears. Unlike the buyer's applyHidden, the sections stay rendered.
1881
+ */
1882
+ setDimmedSections(ids) {
1883
+ const next = new Set(ids ?? []);
1884
+ if (next.size === this.dimmedSections.size && [...next].every((i) => this.dimmedSections.has(i))) return;
1885
+ this.dimmedSections = next;
1886
+ for (const seat of this.seats) {
1887
+ const c = this.circleById.get(seat.id);
1888
+ if (c) this.paintSeat(c, seat.id);
1889
+ }
1890
+ if (this.cached) {
1891
+ this.seatLayer.clearCache();
1892
+ this.cacheSeatLayer();
1893
+ } else {
1894
+ this.seatLayer.batchDraw();
1895
+ }
1226
1896
  }
1227
1897
  /** Legend hover: highlight one category (dim the rest), or null to clear. */
1228
1898
  setCategoryHighlight(key) {
@@ -1405,7 +2075,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1405
2075
  }
1406
2076
  /** Prominent stage caption: uppercase, letter-spaced, larger, softly dimmed. */
1407
2077
  addStageLabel(x, y, text) {
1408
- const t = new import_Text.Text({
2078
+ const t2 = new import_Text.Text({
1409
2079
  x,
1410
2080
  y,
1411
2081
  text: text.toUpperCase(),
@@ -1417,9 +2087,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1417
2087
  listening: false,
1418
2088
  perfectDrawEnabled: false
1419
2089
  });
1420
- t.offsetX(t.width() / 2);
1421
- t.offsetY(t.height() / 2);
1422
- this.bgLayer.add(t);
2090
+ t2.offsetX(t2.width() / 2);
2091
+ t2.offsetY(t2.height() / 2);
2092
+ this.bgLayer.add(t2);
1423
2093
  }
1424
2094
  renderGA(obj) {
1425
2095
  const color = this.catColor.get(obj.categoryKey) ?? "#6e7bff";
@@ -1500,12 +2170,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1500
2170
  this.seatLayer.add(liftGroupSeat);
1501
2171
  }
1502
2172
  const bgTarget = liftGroupBg ?? this.bgLayer;
2173
+ const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
1503
2174
  const outlinePoly = new import_Line.Line({
1504
2175
  points: pts,
1505
2176
  closed: true,
1506
- stroke: "#3a4358",
1507
- strokeWidth: 1.5,
1508
- fill: rgba(obj.color ?? "#3a4358", 0.06),
2177
+ stroke: rgba(outlineTint, 0.5),
2178
+ strokeWidth: 1.75,
2179
+ fill: rgba(outlineTint, 0.08),
2180
+ lineJoin: "round",
1509
2181
  listening: false,
1510
2182
  perfectDrawEnabled: false
1511
2183
  });
@@ -1521,6 +2193,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1521
2193
  perfectDrawEnabled: false
1522
2194
  });
1523
2195
  bgTarget.add(blockPoly);
2196
+ const rowSeats = /* @__PURE__ */ new Map();
2197
+ for (const id of memberIds) {
2198
+ const s = this.seatById.get(id);
2199
+ if (!s) continue;
2200
+ const i = Number(id.slice(id.lastIndexOf(":") + 1)) || 0;
2201
+ (rowSeats.get(s.rowId) ?? rowSeats.set(s.rowId, []).get(s.rowId)).push({ i, x: s.x, y: s.y });
2202
+ }
2203
+ const rowLines = [];
2204
+ for (const arr of rowSeats.values()) {
2205
+ if (arr.length < 2) continue;
2206
+ arr.sort((a, b) => a.i - b.i);
2207
+ const line = new import_Line.Line({
2208
+ points: arr.flatMap((p) => [p.x, p.y]),
2209
+ stroke: rgba("#ffffff", 0.34),
2210
+ strokeWidth: SEAT_RADIUS * 0.55,
2211
+ lineCap: "round",
2212
+ lineJoin: "round",
2213
+ opacity: 0,
2214
+ listening: false,
2215
+ perfectDrawEnabled: false
2216
+ });
2217
+ rowLines.push(line);
2218
+ bgTarget.add(line);
2219
+ }
1524
2220
  const nameLabel = new import_Text.Text({
1525
2221
  x: centroid.x,
1526
2222
  y: centroid.y,
@@ -1529,6 +2225,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1529
2225
  fontStyle: "700",
1530
2226
  fontFamily: this.labelFont(),
1531
2227
  fill: "#8b93a7",
2228
+ // Dark halo so the label reads over the seat dots at any zoom.
2229
+ shadowColor: "#05070c",
2230
+ shadowBlur: 6,
2231
+ shadowOpacity: 0.9,
2232
+ shadowForStrokeEnabled: false,
1532
2233
  listening: false,
1533
2234
  perfectDrawEnabled: false
1534
2235
  });
@@ -1538,11 +2239,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1538
2239
  const subLabel = new import_Text.Text({
1539
2240
  x: centroid.x,
1540
2241
  y: centroid.y,
1541
- text: `${free} LEFT`,
2242
+ text: t("map.seatsLeft", { count: free }),
1542
2243
  fontSize: 12,
1543
- fontStyle: "600",
2244
+ fontStyle: "700",
1544
2245
  fontFamily: "JetBrains Mono, ui-monospace, monospace",
1545
- fill: rgba("#e6e9f0", 0.8),
2246
+ fill: "#f4f6fb",
2247
+ shadowColor: "#05070c",
2248
+ shadowBlur: 5,
2249
+ shadowOpacity: 0.9,
2250
+ shadowForStrokeEnabled: false,
1546
2251
  opacity: 0,
1547
2252
  listening: false,
1548
2253
  perfectDrawEnabled: false
@@ -1561,6 +2266,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1561
2266
  baseFill,
1562
2267
  outlinePoly,
1563
2268
  blockPoly,
2269
+ rowLines,
1564
2270
  nameLabel,
1565
2271
  subLabel,
1566
2272
  elevation,
@@ -1576,7 +2282,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1576
2282
  refreshSectionFill(sec) {
1577
2283
  const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
1578
2284
  sec.blockPoly.fill(darken(sec.baseFill, sold * SOLD_DARKEN));
1579
- sec.subLabel.text(`${sec.free} LEFT`);
2285
+ sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
1580
2286
  sec.subLabel.offsetX(sec.subLabel.width() / 2);
1581
2287
  }
1582
2288
  /**
@@ -1594,8 +2300,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1594
2300
  for (const z of doc.zones) {
1595
2301
  const members = byZone.get(z.id);
1596
2302
  if (!members || !members.length) continue;
1597
- const cx = members.reduce((a, s) => a + s.centroid.x, 0) / members.length;
1598
- const cy = members.reduce((a, s) => a + s.centroid.y, 0) / members.length;
2303
+ let anchor = members[0];
2304
+ for (const s of members) if (s.centroid.y < anchor.centroid.y) anchor = s;
2305
+ const cx = anchor.centroid.x;
2306
+ const cy = anchor.centroid.y;
1599
2307
  let minPrice = Infinity;
1600
2308
  for (const sec of members) {
1601
2309
  for (const id of sec.memberIds) {
@@ -1613,6 +2321,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1613
2321
  letterSpacing: 2,
1614
2322
  fontFamily: this.labelFont(),
1615
2323
  fill: z.color ?? "#f2f4f8",
2324
+ shadowColor: "#05070c",
2325
+ shadowBlur: 10,
2326
+ shadowOpacity: 0.95,
2327
+ shadowForStrokeEnabled: false,
1616
2328
  opacity: 0,
1617
2329
  listening: false,
1618
2330
  perfectDrawEnabled: false
@@ -1625,7 +2337,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1625
2337
  sub = new import_Text.Text({
1626
2338
  x: cx,
1627
2339
  y: cy,
1628
- text: `FROM $${minPrice}`,
2340
+ text: t("map.fromPrice", { price: formatMoney(minPrice, this.currency) }),
1629
2341
  fontSize: 14,
1630
2342
  fontStyle: "600",
1631
2343
  fontFamily: "JetBrains Mono, ui-monospace, monospace",
@@ -1656,7 +2368,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1656
2368
  blockT = scale <= SECTION_PROMINENT_SCALE ? 1 : 0;
1657
2369
  zoneT = scale <= ZONE_PROMINENT_SCALE ? 1 : 0;
1658
2370
  } else {
1659
- blockT = clamp((CACHE_THRESHOLD - scale) / (CACHE_THRESHOLD - SECTION_PROMINENT_SCALE), 0, 1);
2371
+ blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
1660
2372
  zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
1661
2373
  }
1662
2374
  if (!this.zones.length) zoneT = 0;
@@ -1666,7 +2378,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1666
2378
  if (rescale) this.lodScale = scale;
1667
2379
  for (const sec of this.sections) {
1668
2380
  sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT);
1669
- sec.nameLabel.fill(lerpColor("#8b93a7", "#f2f4f8", blockT));
2381
+ for (const line of sec.rowLines) line.opacity(blockT * (1 - zoneT));
2382
+ sec.nameLabel.fill(lerpColor("#aab3c5", "#ffffff", blockT));
1670
2383
  sec.nameLabel.opacity(1 - zoneT);
1671
2384
  sec.subLabel.opacity(blockT * (1 - zoneT));
1672
2385
  if (rescale) {
@@ -1674,9 +2387,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1674
2387
  this.sizeLabel(sec.subLabel, SECTION_SUB_PX / sx, sec.centroid.y + SECTION_LABEL_PX / sx);
1675
2388
  }
1676
2389
  }
2390
+ const zoneOpacity = zoneT * (1 - this.isoT);
1677
2391
  for (const zone of this.zones) {
1678
- zone.label.opacity(zoneT);
1679
- if (zone.sub) zone.sub.opacity(zoneT);
2392
+ zone.label.opacity(zoneOpacity);
2393
+ if (zone.sub) zone.sub.opacity(zoneOpacity);
1680
2394
  if (rescale) {
1681
2395
  const cy = zone.label.y();
1682
2396
  this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, cy);
@@ -1686,11 +2400,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1686
2400
  this.bgLayer.batchDraw();
1687
2401
  }
1688
2402
  /** Set a centred label's world fontSize (for a target screen px) and re-anchor it. */
1689
- sizeLabel(t, fontSize, y) {
1690
- t.fontSize(Math.max(1, fontSize));
1691
- t.offsetX(t.width() / 2);
1692
- t.offsetY(t.height() / 2);
1693
- t.y(y);
2403
+ sizeLabel(t2, fontSize, y) {
2404
+ t2.fontSize(Math.max(1, fontSize));
2405
+ t2.offsetX(t2.width() / 2);
2406
+ t2.offsetY(t2.height() / 2);
2407
+ t2.y(y);
1694
2408
  }
1695
2409
  /**
1696
2410
  * Map a container-relative screen point back to world coords. Inverts the
@@ -1714,7 +2428,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1714
2428
  return this.sections.find((s) => s.id === id)?.memberIds.slice() ?? [];
1715
2429
  }
1716
2430
  addCentredLabel(layer, text, x, y, fill, fontSize, bold) {
1717
- const t = new import_Text.Text({
2431
+ const t2 = new import_Text.Text({
1718
2432
  x,
1719
2433
  y,
1720
2434
  text,
@@ -1725,9 +2439,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1725
2439
  listening: false,
1726
2440
  perfectDrawEnabled: false
1727
2441
  });
1728
- t.offsetX(t.width() / 2);
1729
- t.offsetY(t.height() / 2);
1730
- layer.add(t);
2442
+ t2.offsetX(t2.width() / 2);
2443
+ t2.offsetY(t2.height() / 2);
2444
+ layer.add(t2);
1731
2445
  }
1732
2446
  // ---- selection ------------------------------------------------------------
1733
2447
  isSelectable(id) {
@@ -1781,7 +2495,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1781
2495
  this.seatLayer.on("click tap", (e) => {
1782
2496
  if (this.moved > 8) return;
1783
2497
  const id = seatIdOf(e.target);
1784
- if (id) this.toggleSeat(id);
2498
+ if (!id) return;
2499
+ if (this.stacked && this.opts.onDeckTap) {
2500
+ const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
2501
+ if (floorId) {
2502
+ this.opts.onDeckTap(floorId);
2503
+ return;
2504
+ }
2505
+ }
2506
+ this.toggleSeat(id);
1785
2507
  });
1786
2508
  this.seatLayer.on("mouseover", (e) => {
1787
2509
  const id = seatIdOf(e.target);
@@ -1811,11 +2533,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1811
2533
  if (!this.cached || this.moved > 8) return;
1812
2534
  const pointer = this.stage.getPointerPosition();
1813
2535
  if (!pointer) return;
2536
+ if (this.stacked && this.opts.onDeckTap) {
2537
+ const floorId = this.deckFloorAt();
2538
+ if (floorId) {
2539
+ this.opts.onDeckTap(floorId);
2540
+ return;
2541
+ }
2542
+ }
1814
2543
  if (this.sections.length) {
1815
2544
  const world = this.screenToWorld(pointer);
1816
2545
  const hit = this.sections.find((sn) => pointInPolygon(world, sn.outline));
1817
2546
  if (hit) {
1818
- this.zoomToBounds(polyBounds(hit.outline));
2547
+ if (this.opts.onSectionTap) this.opts.onSectionTap(hit.id);
2548
+ else this.focusRegion(hit.id);
1819
2549
  return;
1820
2550
  }
1821
2551
  }
@@ -1823,6 +2553,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1823
2553
  this.zoomAbout(target, pointer);
1824
2554
  });
1825
2555
  }
2556
+ /**
2557
+ * Which deck (floor) a screen tap landed on in the 3D stacked overview. The
2558
+ * seat layer is melt-cached at that zoom so individual seat nodes aren't
2559
+ * hit-testable; instead we invert the iso+stage transform via the layer's
2560
+ * relative pointer and take the nearest seat's floor. Only the floor matters,
2561
+ * so within-floor elevation offsets don't affect the result. Null if none.
2562
+ */
2563
+ deckFloorAt() {
2564
+ if (!this.objectFloor.size) return null;
2565
+ const p = this.seatLayer.getRelativePointerPosition();
2566
+ if (!p) return null;
2567
+ let best = null;
2568
+ let bestD = Infinity;
2569
+ for (const s of this.seats) {
2570
+ const dx = s.x - p.x;
2571
+ const dy = s.y - p.y;
2572
+ const d = dx * dx + dy * dy;
2573
+ if (d < bestD) {
2574
+ bestD = d;
2575
+ best = s.rowId;
2576
+ }
2577
+ }
2578
+ return best ? this.objectFloor.get(best) ?? null : null;
2579
+ }
1826
2580
  // ---- Pan & pinch (raw pointer events — mouse, touch and pen alike) --------
1827
2581
  toLocal(e) {
1828
2582
  const r = this.container.getBoundingClientRect();
@@ -1833,7 +2587,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1833
2587
  * reach a readable size on any chart, however large the venue.
1834
2588
  */
1835
2589
  zoomBounds() {
1836
- return { min: 0.5 * this.fitScale, max: Math.max(10 * this.fitScale, 4) };
2590
+ let min = 0.5 * this.fitScale;
2591
+ if (this.zones.length) min = Math.min(min, ZONE_PROMINENT_SCALE * 0.9);
2592
+ return { min, max: Math.max(10 * this.fitScale, 4) };
1837
2593
  }
1838
2594
  /** Zoom so `clientPoint` (stage-relative px) stays fixed under the cursor. */
1839
2595
  zoomAbout(nextScale, clientPoint) {
@@ -1866,6 +2622,92 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1866
2622
  this.afterViewChange();
1867
2623
  this.stage.batchDraw();
1868
2624
  }
2625
+ /**
2626
+ * Smoothly glide the camera to frame a section (by id) or a world-space bounds
2627
+ * rect — the Slice 5 "glide in". Pan+zoom tween over ~450ms easeInOutCubic; the
2628
+ * melt/LOD rides the camera every frame. `prefers-reduced-motion` (or
2629
+ * `opts.animate === false`) snaps via zoomToBounds. A grab (pointer-down) or a
2630
+ * newer glide cancels an in-flight one.
2631
+ */
2632
+ focusRegion(target, opts) {
2633
+ const b = typeof target === "string" ? (() => {
2634
+ const sec = this.sections.find((s) => s.id === target);
2635
+ return sec ? polyBounds(sec.outline) : null;
2636
+ })() : target;
2637
+ if (!b) return;
2638
+ this.cancelGlide();
2639
+ if (opts?.animate === false || this.reducedMotion) {
2640
+ this.zoomToBounds(b);
2641
+ return;
2642
+ }
2643
+ const w = this.stage.width();
2644
+ const h = this.stage.height();
2645
+ const { min, max } = this.zoomBounds();
2646
+ const margin = 1.12;
2647
+ const toScale = clamp(Math.min(w / (b.width * margin), h / (b.height * margin)), min, max);
2648
+ const toX = w / 2 - (b.x + b.width / 2) * toScale;
2649
+ const toY = h / 2 - (b.y + b.height / 2) * toScale;
2650
+ const fromScale = this.stage.scaleX();
2651
+ const fromX = this.stage.x();
2652
+ const fromY = this.stage.y();
2653
+ if (Math.abs(toScale - fromScale) < 1e-4 && Math.abs(toX - fromX) < 0.5 && Math.abs(toY - fromY) < 0.5) {
2654
+ this.afterViewChange();
2655
+ return;
2656
+ }
2657
+ const start = performance.now();
2658
+ const DUR = 450;
2659
+ const step = (now) => {
2660
+ if (this.destroyed) {
2661
+ this.glideRaf = 0;
2662
+ return;
2663
+ }
2664
+ const raw = Math.min(1, (now - start) / DUR);
2665
+ const e = raw < 0.5 ? 4 * raw * raw * raw : 1 - Math.pow(-2 * raw + 2, 3) / 2;
2666
+ const sc = fromScale + (toScale - fromScale) * e;
2667
+ this.stage.scale({ x: sc, y: sc });
2668
+ this.stage.position({ x: fromX + (toX - fromX) * e, y: fromY + (toY - fromY) * e });
2669
+ this.updateLOD();
2670
+ this.scheduleViewChange();
2671
+ this.stage.batchDraw();
2672
+ if (raw < 1) {
2673
+ this.glideRaf = requestAnimationFrame(step);
2674
+ } else {
2675
+ this.glideRaf = 0;
2676
+ this.afterViewChange();
2677
+ }
2678
+ };
2679
+ this.glideRaf = requestAnimationFrame(step);
2680
+ }
2681
+ /** Cancel an in-flight camera glide (a grab or a newer glide interrupts it). */
2682
+ cancelGlide() {
2683
+ if (this.glideRaf) {
2684
+ cancelAnimationFrame(this.glideRaf);
2685
+ this.glideRaf = 0;
2686
+ }
2687
+ }
2688
+ /** Current LOD rung derived from effective zoom — drives the ZONES/SECTIONS/SEATS pill. */
2689
+ getRung() {
2690
+ const scale = this.effScale();
2691
+ if (scale >= CACHE_THRESHOLD) return "seats";
2692
+ if (this.zones.length && scale < ZONE_PROMINENT_SCALE) return "zones";
2693
+ return "sections";
2694
+ }
2695
+ /** Jump the camera to a rung's zoom band, centred on the chart (glided). */
2696
+ setRung(rung) {
2697
+ if (rung === "zones") {
2698
+ this.cancelGlide();
2699
+ this.zoomToFit();
2700
+ return;
2701
+ }
2702
+ const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(SEAT_LEGIBLE_SCALE * 1.1, CACHE_THRESHOLD * 1.3);
2703
+ const w = this.stage.width();
2704
+ const h = this.stage.height();
2705
+ const cx = this.bounds.x + this.bounds.width / 2;
2706
+ const cy = this.bounds.y + this.bounds.height / 2;
2707
+ const bw = w / (target * 1.12);
2708
+ const bh = h / (target * 1.12);
2709
+ this.focusRegion({ x: cx - bw / 2, y: cy - bh / 2, width: bw, height: bh });
2710
+ }
1869
2711
  /** Recompute LOD (cache/labels) after any pan/zoom settles. */
1870
2712
  afterViewChange() {
1871
2713
  this.updateLOD();
@@ -1916,7 +2758,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1916
2758
  let count = 0;
1917
2759
  for (const seat of this.seats) {
1918
2760
  if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
1919
- const t = new import_Text.Text({
2761
+ const t2 = new import_Text.Text({
1920
2762
  x: seat.x,
1921
2763
  y: seat.y,
1922
2764
  text: seat.label,
@@ -1928,14 +2770,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1928
2770
  perfectDrawEnabled: false
1929
2771
  });
1930
2772
  const maxW = this.seatR * 2 - 3;
1931
- if (t.width() > maxW) t.fontSize(Math.max(4, 7 * maxW / t.width()));
1932
- if (t.fontSize() < 4.2) {
1933
- t.destroy();
2773
+ if (t2.width() > maxW) t2.fontSize(Math.max(4, 7 * maxW / t2.width()));
2774
+ if (t2.fontSize() < 4.2) {
2775
+ t2.destroy();
1934
2776
  continue;
1935
2777
  }
1936
- t.offsetX(t.width() / 2);
1937
- t.offsetY(t.height() / 2);
1938
- this.labelGroup.add(t);
2778
+ t2.offsetX(t2.width() / 2);
2779
+ t2.offsetY(t2.height() / 2);
2780
+ this.labelGroup.add(t2);
1939
2781
  if (++count >= MAX_LABELS) break;
1940
2782
  }
1941
2783
  if (this.isoT > 0) this.applyUprightLabels();
@@ -1970,6 +2812,38 @@ function createRenderer(container, opts) {
1970
2812
  return new SeatmapRenderer(container, opts);
1971
2813
  }
1972
2814
 
2815
+ // src/core/orphans.ts
2816
+ function seatIndex(id) {
2817
+ const n = Number(id.slice(id.lastIndexOf(":") + 1));
2818
+ return Number.isFinite(n) ? n : -1;
2819
+ }
2820
+ function strandedSingles(seats, statusOf, selectedIds) {
2821
+ const rows = /* @__PURE__ */ new Map();
2822
+ for (const s of seats) {
2823
+ if (s.kind === "booth") continue;
2824
+ const list = rows.get(s.rowId);
2825
+ if (list) list.push(s);
2826
+ else rows.set(s.rowId, [s]);
2827
+ }
2828
+ const unavailable = (s) => selectedIds.has(s.id) || statusOf(s.id) !== "free";
2829
+ const out = [];
2830
+ for (const list of rows.values()) {
2831
+ if (list.length < 3) continue;
2832
+ list.sort((a, b) => seatIndex(a.id) - seatIndex(b.id));
2833
+ for (let i = 1; i < list.length - 1; i++) {
2834
+ const seat = list[i];
2835
+ if (unavailable(seat)) continue;
2836
+ const left = list[i - 1];
2837
+ const right = list[i + 1];
2838
+ if (seatIndex(seat.id) - seatIndex(left.id) !== 1 || seatIndex(right.id) - seatIndex(seat.id) !== 1) continue;
2839
+ if (!unavailable(left) || !unavailable(right)) continue;
2840
+ if (!selectedIds.has(left.id) && !selectedIds.has(right.id)) continue;
2841
+ out.push(seat);
2842
+ }
2843
+ }
2844
+ return out;
2845
+ }
2846
+
1973
2847
  // src/picker/PickerController.ts
1974
2848
  var DEFAULT_MAX_SELECTION = 10;
1975
2849
  var MAX_BACKOFF_MS = 15e3;
@@ -1986,9 +2860,15 @@ var PickerController = class {
1986
2860
  constructor(options) {
1987
2861
  this.renderer = null;
1988
2862
  this._doc = null;
2863
+ /** Section/zone ids hidden from buyers this event (3.3) — seats vanish, not grey. */
2864
+ this.hidden = /* @__PURE__ */ new Set();
1989
2865
  /** label ⇄ id maps — backend speaks labels, the engine speaks ids. */
1990
2866
  this.labelToId = /* @__PURE__ */ new Map();
1991
2867
  this.labelToSeat = /* @__PURE__ */ new Map();
2868
+ /** seatId → chosen ticket-tier id (absent ⇒ the category's first/default tier). */
2869
+ this.seatTiers = /* @__PURE__ */ new Map();
2870
+ /** id → seat, for the section-summary breakdown (renderer members are ids). */
2871
+ this.seatById = /* @__PURE__ */ new Map();
1992
2872
  this.allIds = [];
1993
2873
  // realtime socket
1994
2874
  this.ws = null;
@@ -1998,6 +2878,8 @@ var PickerController = class {
1998
2878
  // hold state
1999
2879
  this.hold_ = null;
2000
2880
  this.expiryTimer = null;
2881
+ /** Whether the last emitted hint was non-null (avoids clearing repeatedly). */
2882
+ this.hintShown = false;
2001
2883
  this.opts = options;
2002
2884
  this.api = options.transport;
2003
2885
  this.key = options.eventKey;
@@ -2006,6 +2888,18 @@ var PickerController = class {
2006
2888
  get doc() {
2007
2889
  return this._doc;
2008
2890
  }
2891
+ /** The chart with hidden sections' seats removed — what buyers actually see. */
2892
+ visibleDoc() {
2893
+ return this._doc ? applyHidden(this._doc, this.hidden) : { objects: [] };
2894
+ }
2895
+ /** Adopt a new hidden set; rebuild the visible chart only if it differs. */
2896
+ syncHidden(ids) {
2897
+ const next = ids.filter((x) => typeof x === "string");
2898
+ if (next.length === this.hidden.size && next.every((id) => this.hidden.has(id))) return false;
2899
+ this.hidden = new Set(next);
2900
+ this.renderer?.setChart(this.visibleDoc());
2901
+ return true;
2902
+ }
2009
2903
  currentHold() {
2010
2904
  return this.hold_;
2011
2905
  }
@@ -2033,15 +2927,19 @@ var PickerController = class {
2033
2927
  this._doc = res.doc;
2034
2928
  this.labelToId = /* @__PURE__ */ new Map();
2035
2929
  this.labelToSeat = /* @__PURE__ */ new Map();
2930
+ this.seatById = /* @__PURE__ */ new Map();
2036
2931
  this.allIds = [];
2037
2932
  for (const s of expandChart(res.doc)) {
2038
2933
  this.labelToId.set(s.label, s.id);
2039
2934
  this.labelToSeat.set(s.label, s);
2935
+ this.seatById.set(s.id, s);
2040
2936
  this.allIds.push(s.id);
2041
2937
  }
2938
+ const currency = res.event.currency ?? this.opts.currency;
2042
2939
  const renderer = createRenderer(host, {
2043
2940
  maxSelection: this.maxSelection,
2044
2941
  confirmSelection: this.opts.confirmSelection,
2942
+ currency,
2045
2943
  onSelect: (seat) => {
2046
2944
  this.opts.onSelect?.(seat);
2047
2945
  this.emitSelectionChange();
@@ -2054,6 +2952,9 @@ var PickerController = class {
2054
2952
  onFocusSeat: this.opts.onFocusSeat,
2055
2953
  onViewChange: this.opts.onViewChange,
2056
2954
  onGAClick: this.opts.onGAClick,
2955
+ // Section tap at the far/zone rung → glide in + surface the summary card.
2956
+ onSectionTap: (id) => this.handleSectionTap(id),
2957
+ onDeckTap: (floorId) => this.handleDeckTap(floorId),
2057
2958
  onFps: this.opts.onFps
2058
2959
  });
2059
2960
  if (this.closed) {
@@ -2061,15 +2962,29 @@ var PickerController = class {
2061
2962
  return null;
2062
2963
  }
2063
2964
  this.renderer = renderer;
2064
- renderer.setChart(res.doc);
2065
- await this.resnapshot();
2965
+ let seats = null;
2966
+ try {
2967
+ const objs = await this.api.objects(this.key);
2968
+ this.hidden = new Set(objs.hidden ?? []);
2969
+ seats = objs.seats;
2970
+ } catch {
2971
+ }
2972
+ if (this.closed) {
2973
+ renderer.destroy();
2974
+ this.renderer = null;
2975
+ return null;
2976
+ }
2977
+ renderer.setChart(this.visibleDoc());
2978
+ if (this.opts.colorblindSafe) renderer.setColorblindSafe?.(true);
2979
+ if (seats) this.applySeatsMap(seats);
2066
2980
  this.connect();
2067
2981
  return {
2068
2982
  doc: res.doc,
2069
2983
  salesClosed: !!res.event.salesClosed,
2070
2984
  eventName: res.event.name,
2071
2985
  venue: res.event.venue,
2072
- startsAt: res.event.startsAt
2986
+ startsAt: res.event.startsAt,
2987
+ currency
2073
2988
  };
2074
2989
  }
2075
2990
  // ---- selection ------------------------------------------------------------
@@ -2241,6 +3156,126 @@ var PickerController = class {
2241
3156
  setAccessibilityFilter(types) {
2242
3157
  this.renderer?.setAccessibilityFilter?.(types);
2243
3158
  }
3159
+ // ---- multi-floor (Batch 5) ------------------------------------------------
3160
+ /** Floors for the buyer's switcher (>1 ⇒ show it). Single-floor ⇒ one entry. */
3161
+ getFloors() {
3162
+ return this.renderer?.getFloors?.() ?? [];
3163
+ }
3164
+ getActiveFloorId() {
3165
+ return this.renderer?.getActiveFloorId?.() ?? "";
3166
+ }
3167
+ /** Switch the shown floor (2D), then re-apply live seat statuses onto it. */
3168
+ setFloor(id) {
3169
+ const r = this.renderer;
3170
+ if (!r?.setActiveFloor || id === r.getActiveFloorId?.()) return;
3171
+ r.setStacked?.(false);
3172
+ r.setActiveFloor(id);
3173
+ void this.resnapshot();
3174
+ this.emitSelectionChange();
3175
+ }
3176
+ /** 3D all-floors stacked overview ⇄ active floor. Re-applies statuses after. */
3177
+ setStacked(on) {
3178
+ const r = this.renderer;
3179
+ if (!r?.setStacked || on === r.isStacked?.()) return;
3180
+ r.setStacked(on);
3181
+ void this.resnapshot();
3182
+ }
3183
+ isMultiFloor() {
3184
+ return (this._doc?.floors?.length ?? 0) > 1;
3185
+ }
3186
+ /** Tap-a-deck-to-enter: leave the 3D stack, drop to flat 2D on `floorId`, and
3187
+ * tell the host so it can sync its 2D/3D toggle + floor-switcher state. */
3188
+ handleDeckTap(floorId) {
3189
+ const r = this.renderer;
3190
+ if (!r) return;
3191
+ r.setViewMode?.("flat");
3192
+ r.setStacked?.(false);
3193
+ r.setActiveFloor?.(floorId);
3194
+ void this.resnapshot();
3195
+ this.emitSelectionChange();
3196
+ this.opts.onDeckTap?.(floorId);
3197
+ }
3198
+ // ---- big-venue: sections / rungs / projection (Slice 5) -------------------
3199
+ /** Switch the map projection (2D flat ⇄ 3D isometric). No-op on a flat renderer. */
3200
+ setViewMode(mode) {
3201
+ this.renderer?.setViewMode?.(mode);
3202
+ }
3203
+ getViewMode() {
3204
+ return this.renderer?.getViewMode?.() ?? "flat";
3205
+ }
3206
+ /** Current LOD rung (for the ZONES/SECTIONS/SEATS pill). */
3207
+ getRung() {
3208
+ return this.renderer?.getRung?.() ?? "seats";
3209
+ }
3210
+ /** Jump to a rung; ZONES clears any focused summary (back to overview). */
3211
+ setRung(rung) {
3212
+ if (rung === "zones") {
3213
+ this.overview();
3214
+ return;
3215
+ }
3216
+ this.renderer?.setRung?.(rung);
3217
+ }
3218
+ /** Glide in on a section and surface its summary (same path as a section tap). */
3219
+ focusSection(id) {
3220
+ this.handleSectionTap(id);
3221
+ }
3222
+ /** Zoom back out to the whole chart and clear the section-summary card. */
3223
+ overview() {
3224
+ this.renderer?.zoomToFit();
3225
+ this.opts.onSectionFocus?.(null);
3226
+ }
3227
+ /** Glide the camera into a tapped section and emit its computed summary. */
3228
+ handleSectionTap(id) {
3229
+ const r = this.renderer;
3230
+ if (!r) return;
3231
+ r.focusRegion?.(id);
3232
+ this.opts.onSectionFocus?.(this.sectionSummary(id));
3233
+ }
3234
+ /**
3235
+ * Build a section summary from the renderer's spatial membership: section +
3236
+ * zone labels, live seats-left, price range, and the per-category breakdown.
3237
+ */
3238
+ sectionSummary(id) {
3239
+ const r = this.renderer;
3240
+ const doc = this._doc;
3241
+ if (!r || !doc) return null;
3242
+ const sec = doc.objects.find(
3243
+ (o) => o.type === "section" && o.id === id
3244
+ );
3245
+ if (!sec) return null;
3246
+ const memberIds = r.sectionMembers?.(id) ?? [];
3247
+ const byCat = /* @__PURE__ */ new Map();
3248
+ let seatsLeft = 0;
3249
+ for (const sid of memberIds) {
3250
+ const seat = this.seatById.get(sid);
3251
+ if (!seat) continue;
3252
+ byCat.set(seat.categoryKey, (byCat.get(seat.categoryKey) ?? 0) + 1);
3253
+ if (r.getStatus(sid) === "free") seatsLeft++;
3254
+ }
3255
+ const categories = [...byCat.entries()].map(([key, count]) => {
3256
+ const cat = doc.categories.find((c) => c.key === key);
3257
+ return {
3258
+ key,
3259
+ count,
3260
+ label: cat?.label ?? key,
3261
+ color: cat?.color ?? "#6e7bff",
3262
+ price: cat?.price ?? 0
3263
+ };
3264
+ }).sort((a, b) => a.price - b.price);
3265
+ const prices = categories.map((c) => c.price);
3266
+ const zone = sec.zone ? doc.zones?.find((z) => z.id === sec.zone) : void 0;
3267
+ const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
3268
+ return {
3269
+ id,
3270
+ label: sec.label,
3271
+ zoneLabel: zone?.label ?? "",
3272
+ color,
3273
+ seatsLeft,
3274
+ priceMin: prices.length ? prices[0] : 0,
3275
+ priceMax: prices.length ? prices[prices.length - 1] : 0,
3276
+ categories
3277
+ };
3278
+ }
2244
3279
  destroy() {
2245
3280
  this.closed = true;
2246
3281
  if (this.reconnectTimer) {
@@ -2263,13 +3298,80 @@ var PickerController = class {
2263
3298
  }
2264
3299
  // ---- internals ------------------------------------------------------------
2265
3300
  toSeat(s) {
2266
- return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey) };
3301
+ const tiers = this.tiersFor(s.categoryKey);
3302
+ if (!tiers) {
3303
+ return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey) };
3304
+ }
3305
+ const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
3306
+ return {
3307
+ id: s.id,
3308
+ label: s.label,
3309
+ categoryKey: s.categoryKey,
3310
+ price: chosen.price,
3311
+ tiers,
3312
+ tierId: chosen.id
3313
+ };
2267
3314
  }
2268
3315
  priceFor(categoryKey) {
2269
3316
  return this._doc?.categories.find((c) => c.key === categoryKey)?.price ?? 0;
2270
3317
  }
3318
+ tiersFor(categoryKey) {
3319
+ const t2 = this._doc?.categories.find((c) => c.key === categoryKey)?.tiers;
3320
+ return t2 && t2.length ? t2 : void 0;
3321
+ }
3322
+ /**
3323
+ * Choose a ticket tier for a selected seat (e.g. Adult → Child). Re-emits the
3324
+ * selection so the host's cart + the eventual hold carry the new tier + price.
3325
+ * `tierId = null` reverts to the category's first (default) tier. No-op if the
3326
+ * seat's category has no tiers or the id isn't one of them.
3327
+ */
3328
+ setSeatTier(seatId, tierId) {
3329
+ const seat = this.seatById.get(seatId);
3330
+ if (!seat) return;
3331
+ const tiers = this.tiersFor(seat.categoryKey);
3332
+ if (!tiers) return;
3333
+ if (tierId == null) this.seatTiers.delete(seatId);
3334
+ else if (tiers.some((t2) => t2.id === tierId)) this.seatTiers.set(seatId, tierId);
3335
+ else return;
3336
+ this.emitSelectionChange();
3337
+ if (this.hold_) this.hold_ = { ...this.hold_, seats: this.seatsForLabels(this.hold_.labels) };
3338
+ }
3339
+ /** PickerSeats (with chosen tier) for a set of held labels. */
3340
+ seatsForLabels(labels) {
3341
+ const out = [];
3342
+ for (const l of labels) {
3343
+ const s = this.labelToSeat.get(l);
3344
+ if (s) out.push(this.toSeat(s));
3345
+ }
3346
+ return out;
3347
+ }
2271
3348
  emitSelectionChange() {
2272
3349
  this.opts.onSelectionChange?.(this.getSelection());
3350
+ this.emitOrphanHint();
3351
+ }
3352
+ /**
3353
+ * Orphan-seat advice on manual selection: when the buyer's current picks
3354
+ * strand one (or more) single free seats between unavailable same-row
3355
+ * neighbors, surface a localized, non-blocking hint. Cleared (null) as soon
3356
+ * as the selection stops stranding anyone.
3357
+ */
3358
+ emitOrphanHint() {
3359
+ if (!this.opts.onHint) return;
3360
+ const r = this.renderer;
3361
+ if (!r) return;
3362
+ const selected = new Set(r.getSelection().map((s) => s.id));
3363
+ const stranded = selected.size ? strandedSingles(this.seatById.values(), (id) => r.getStatus(id), selected) : [];
3364
+ if (stranded.length > 0) {
3365
+ this.hintShown = true;
3366
+ this.opts.onHint(t("picker.orphanHint"));
3367
+ } else if (this.hintShown) {
3368
+ this.hintShown = false;
3369
+ this.opts.onHint(null);
3370
+ }
3371
+ }
3372
+ /** Toggle colorblind-safe rendering at runtime (see PickerOptions.colorblindSafe). */
3373
+ setColorblindSafe(on) {
3374
+ this.renderer?.setColorblindSafe?.(on);
2273
3375
  }
2274
3376
  emitError(err) {
2275
3377
  if (this.opts.onError) this.opts.onError(err);
@@ -2293,11 +3395,12 @@ var PickerController = class {
2293
3395
  }
2294
3396
  /** Set the open hold + (re)arm the server-authoritative expiry timer. */
2295
3397
  setHold(hold) {
2296
- this.hold_ = hold;
3398
+ const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
3399
+ this.hold_ = full;
2297
3400
  if (this.expiryTimer) clearTimeout(this.expiryTimer);
2298
- const ms = Math.max(0, hold.expiresAt - Date.now());
3401
+ const ms = Math.max(0, full.expiresAt - Date.now());
2299
3402
  this.expiryTimer = setTimeout(() => this.onHoldExpired(), ms);
2300
- this.opts.onHold?.(hold);
3403
+ this.opts.onHold?.(full);
2301
3404
  }
2302
3405
  clearHold() {
2303
3406
  this.hold_ = null;
@@ -2361,6 +3464,11 @@ var PickerController = class {
2361
3464
  const r = this.renderer;
2362
3465
  if (!r || !msg || typeof msg !== "object") return;
2363
3466
  const m = msg;
3467
+ if (Array.isArray(m.hidden) && this.syncHidden(m.hidden)) {
3468
+ void this.resnapshot();
3469
+ this.opts.onStatusChange?.();
3470
+ }
3471
+ if (m.type === "hidden") return;
2364
3472
  if (m.seats && typeof m.seats === "object") {
2365
3473
  this.applySeatsMap(m.seats);
2366
3474
  } else if (Array.isArray(m.changes)) {
@@ -2397,22 +3505,68 @@ var PickerController = class {
2397
3505
  }, delay);
2398
3506
  }
2399
3507
  };
3508
+
3509
+ // src/i18n/bundles.ts
3510
+ var LOADERS = {
3511
+ es: () => Promise.resolve().then(() => (init_es(), es_exports)).then((m) => ({ default: m.es })),
3512
+ de: () => Promise.resolve().then(() => (init_de(), de_exports)).then((m) => ({ default: m.de })),
3513
+ fr: () => Promise.resolve().then(() => (init_fr(), fr_exports)).then((m) => ({ default: m.fr }))
3514
+ };
3515
+ var loaded = /* @__PURE__ */ new Set(["en"]);
3516
+ async function loadLocale(code) {
3517
+ const locale = resolveLocale(code);
3518
+ if (locale === "en" || loaded.has(locale)) {
3519
+ setLocale(locale);
3520
+ return locale;
3521
+ }
3522
+ try {
3523
+ const mod = await LOADERS[locale]();
3524
+ setLocale(locale, mod.default);
3525
+ loaded.add(locale);
3526
+ return locale;
3527
+ } catch {
3528
+ setLocale("en");
3529
+ return "en";
3530
+ }
3531
+ }
2400
3532
  // Annotate the CommonJS export names for ESM import in node:
2401
3533
  0 && (module.exports = {
2402
3534
  ACCESSIBILITY_TYPES,
2403
3535
  CHART_STORAGE_KEY,
3536
+ DEFAULT_CURRENCY,
2404
3537
  PickerController,
3538
+ SUPPORTED_LOCALES,
2405
3539
  SeatmapRenderer,
3540
+ UNGROUPED_ID,
2406
3541
  accessibilityMeta,
3542
+ allObjects,
3543
+ applyHidden,
2407
3544
  chartBounds,
3545
+ computeSections,
2408
3546
  createRenderer,
3547
+ currencySymbol,
2409
3548
  expandBooth,
2410
3549
  expandChart,
2411
3550
  expandRow,
2412
3551
  expandRowSlots,
2413
3552
  expandTable,
3553
+ floorObjects,
3554
+ floorsOf,
3555
+ formatDate,
3556
+ formatMoney,
3557
+ getLocale,
3558
+ hiddenObjectIds,
3559
+ isSectionHidden,
2414
3560
  layerOf,
3561
+ loadLocale,
2415
3562
  objectCenter,
2416
- pointInPolygon
3563
+ pointInPolygon,
3564
+ resolveLocale,
3565
+ setLocale,
3566
+ setMoneyLocale,
3567
+ setStringOverrides,
3568
+ stackFloors,
3569
+ t,
3570
+ tCount
2417
3571
  });
2418
3572
  //# sourceMappingURL=index.cjs.map