@seatlayer/core 0.1.2 → 0.1.3

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,277 @@ 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 SeatMap",
45
+ "picker.seats.one": "{count} asiento",
46
+ "picker.seats.other": "{count} asientos",
47
+ // renderer (drawn on the Konva map — shared by the embed SDK)
48
+ "map.aria": "Mapa de asientos. Usa las flechas para moverte entre asientos, Intro para seleccionar.",
49
+ "map.seatsLeft": "{count} LIBRES",
50
+ "map.fromPrice": "DESDE {price}",
51
+ // buyer picker page (src/pages/PickerPage.tsx)
52
+ "picker.language": "Idioma",
53
+ "picker.zoomToFit": "Ajustar zoom",
54
+ "picker.viewMode": "Modo de vista",
55
+ "picker.floor": "Piso",
56
+ "picker.zoomLevel": "Nivel de zoom",
57
+ "picker.rungTip.zones": "Nivel m\xE1s lejano \u2014 bloques de zona (Piso / Patio Bajo / Patio Alto)",
58
+ "picker.rungTip.sections": "Bloques de secci\xF3n \u2014 mezcla de categor\xEDas + matiz de disponibilidad",
59
+ "picker.rungTip.seats": "Asientos individuales \u2014 los bloques se convierten en puntos",
60
+ "picker.rungLabel.zones": "ZONAS",
61
+ "picker.rungLabel.sections": "SECCIONES",
62
+ "picker.rungLabel.seats": "ASIENTOS",
63
+ "picker.sectionSummaryAria": "Resumen de secci\xF3n {label}",
64
+ "picker.closeSectionSummary": "Cerrar resumen de secci\xF3n",
65
+ "picker.seatsLeftInSection.one": "{count} asiento disponible",
66
+ "picker.seatsLeftInSection.other": "{count} asientos disponibles",
67
+ "picker.overview": "Descripci\xF3n general",
68
+ "picker.tapSeatHint": "Toca cualquier asiento para ver su vista",
69
+ "picker.chartSize": "Tama\xF1o del gr\xE1fico",
70
+ "picker.custom": "Personalizado",
71
+ "picker.categories": "Categor\xEDas",
72
+ "picker.accessibility": "Accesibilidad",
73
+ "picker.showAnyAccessible": "Mostrar cualquier asiento accesible",
74
+ "picker.any": "Cualquiera",
75
+ "picker.yourSeats": "Tus asientos",
76
+ "picker.emptySeats": "Toca asientos en el mapa",
77
+ "picker.emptySeatsWithGa": "Toca asientos en el mapa \xB7 toca un \xE1rea de pie para entradas de entrada general",
78
+ "picker.oneFewer": "Uno menos",
79
+ "picker.oneMore": "Uno m\xE1s",
80
+ "picker.remove": "Eliminar {label}",
81
+ "picker.ticketTierFor": "Categor\xEDa de entrada para {label}",
82
+ "picker.total": "Total: {amount}",
83
+ "picker.viewFromSeat": "Vista desde el asiento {label}",
84
+ "picker.real360": "REAL 360\xB0",
85
+ "picker.preview": "VISTA PREVIA",
86
+ "picker.open360": "Abrir vista de 360\xB0",
87
+ "picker.sightline": "\u2248 {rows} filas del escenario \xB7 {angle}\xB0 fuera del centro",
88
+ "picker.bookedDemo": "\xA1Reservado! (demostraci\xF3n)",
89
+ "picker.bookButton.one": "Reservar {count} entrada \u2014 {amount}",
90
+ "picker.bookButton.other": "Reservar {count} entradas \u2014 {amount}",
91
+ "picker.simulateCrowd": "Simular multitud: {state}",
92
+ "picker.on": "ACTIVADO",
93
+ "picker.off": "DESACTIVADO",
94
+ "picker.panorama360": "Foto de 360\xB0 del lugar",
95
+ "picker.illustrationCaption": "ilustraci\xF3n \xB7 \u2248 {m} m del escenario"
96
+ };
97
+ }
98
+ });
99
+
100
+ // src/i18n/locales/de.ts
101
+ var de_exports = {};
102
+ __export(de_exports, {
103
+ de: () => de
104
+ });
105
+ var de;
106
+ var init_de = __esm({
107
+ "src/i18n/locales/de.ts"() {
108
+ "use strict";
109
+ de = {
110
+ // shared
111
+ "common.cancel": "Abbrechen",
112
+ "common.close": "Schlie\xDFen",
113
+ "common.done": "Fertig",
114
+ "common.copied": "\u2713 Kopiert",
115
+ // buyer picker (extraction in progress — P4 of the port plan)
116
+ "picker.holdSeats": "Pl\xE4tze reservieren & Kasse",
117
+ "picker.completeBooking": "Buchung abschlie\xDFen",
118
+ "picker.seatsHeld": "Pl\xE4tze reserviert \u2014 {time}",
119
+ "picker.holdExpired": "Ihre Reservierung ist abgelaufen \u2014 die Pl\xE4tze wurden freigegeben. Bitte erneut ausw\xE4hlen.",
120
+ "picker.seatTaken": "Platz {label} wurde gerade von einem anderen K\xE4ufer gebucht.",
121
+ "picker.poweredBy": "Bereitgestellt von SeatMap",
122
+ "picker.seats.one": "{count} Platz",
123
+ "picker.seats.other": "{count} Pl\xE4tze",
124
+ // renderer (drawn on the Konva map — shared by the embed SDK)
125
+ "map.aria": "Sitzplan. Mit Pfeiltasten zwischen Pl\xE4tzen navigieren, Eingabe zum Ausw\xE4hlen.",
126
+ "map.seatsLeft": "{count} FREI",
127
+ "map.fromPrice": "AB {price}",
128
+ // buyer picker page (src/pages/PickerPage.tsx)
129
+ "picker.language": "Sprache",
130
+ "picker.zoomToFit": "Auf Gr\xF6\xDFe anpassen",
131
+ "picker.viewMode": "Ansichtsmodus",
132
+ "picker.floor": "Etage",
133
+ "picker.zoomLevel": "Zoomstufe",
134
+ "picker.rungTip.zones": "Fernste Stufe \u2014 Zonen-Bl\xF6cke (Etage / Unteres Parterre / Oberes Parterre)",
135
+ "picker.rungTip.sections": "Bereichs-Bl\xF6cke \u2014 Kategoriesortierung + Verf\xFCgbarkeitst\xF6nung",
136
+ "picker.rungTip.seats": "Einzelne Pl\xE4tze \u2014 Bl\xF6cke l\xF6sen sich zu Punkten auf",
137
+ "picker.rungLabel.zones": "ZONEN",
138
+ "picker.rungLabel.sections": "BEREICHE",
139
+ "picker.rungLabel.seats": "PL\xC4TZE",
140
+ "picker.sectionSummaryAria": "Bereichszusammenfassung {label}",
141
+ "picker.closeSectionSummary": "Bereichszusammenfassung schlie\xDFen",
142
+ "picker.seatsLeftInSection.one": "{count} Platz verf\xFCgbar",
143
+ "picker.seatsLeftInSection.other": "{count} Pl\xE4tze verf\xFCgbar",
144
+ "picker.overview": "\xDCbersicht",
145
+ "picker.tapSeatHint": "Tippen Sie auf einen Platz, um die Ansicht zu pr\xFCfen",
146
+ "picker.chartSize": "Kartengr\xF6\xDFe",
147
+ "picker.custom": "Benutzerdefiniert",
148
+ "picker.categories": "Kategorien",
149
+ "picker.accessibility": "Barrierefreiheit",
150
+ "picker.showAnyAccessible": "Jeden barrierefreien Platz anzeigen",
151
+ "picker.any": "Beliebig",
152
+ "picker.yourSeats": "Ihre Pl\xE4tze",
153
+ "picker.emptySeats": "Tippen Sie auf die Pl\xE4tze auf der Karte",
154
+ "picker.emptySeatsWithGa": "Tippen Sie auf die Pl\xE4tze auf der Karte \xB7 tippen Sie auf einen Stehbereich f\xFCr Stehplatztickets",
155
+ "picker.oneFewer": "Einen weniger",
156
+ "picker.oneMore": "Einen mehr",
157
+ "picker.remove": "{label} entfernen",
158
+ "picker.ticketTierFor": "Ticketklasse f\xFCr {label}",
159
+ "picker.total": "Gesamt: {amount}",
160
+ "picker.viewFromSeat": "Ansicht von Platz {label}",
161
+ "picker.real360": "REAL 360\xB0",
162
+ "picker.preview": "VORSCHAU",
163
+ "picker.open360": "\xD6ffnen Sie die 360\xB0-Ansicht",
164
+ "picker.sightline": "\u2248 {rows} Reihen von der B\xFChne \xB7 {angle}\xB0 seitlich versetzt",
165
+ "picker.bookedDemo": "Gebucht! (Demo)",
166
+ "picker.bookButton.one": "{count} Ticket buchen \u2014 {amount}",
167
+ "picker.bookButton.other": "{count} Tickets buchen \u2014 {amount}",
168
+ "picker.simulateCrowd": "Menschenmenge simulieren: {state}",
169
+ "picker.on": "AN",
170
+ "picker.off": "AUS",
171
+ "picker.panorama360": "360\xB0-Veranstaltungsfotos",
172
+ "picker.illustrationCaption": "Illustration \xB7 \u2248 {m} m von der B\xFChne"
173
+ };
174
+ }
175
+ });
176
+
177
+ // src/i18n/locales/fr.ts
178
+ var fr_exports = {};
179
+ __export(fr_exports, {
180
+ fr: () => fr
181
+ });
182
+ var fr;
183
+ var init_fr = __esm({
184
+ "src/i18n/locales/fr.ts"() {
185
+ "use strict";
186
+ fr = {
187
+ // shared
188
+ "common.cancel": "Annuler",
189
+ "common.close": "Fermer",
190
+ "common.done": "Termin\xE9",
191
+ "common.copied": "\u2713 Copi\xE9",
192
+ // buyer picker (extraction in progress — P4 of the port plan)
193
+ "picker.holdSeats": "R\xE9server les si\xE8ges et valider",
194
+ "picker.completeBooking": "Terminer la r\xE9servation",
195
+ "picker.seatsHeld": "Si\xE8ges r\xE9serv\xE9s \u2014 {time}",
196
+ "picker.holdExpired": "Votre r\xE9servation a expir\xE9 \u2014 les si\xE8ges ont \xE9t\xE9 lib\xE9r\xE9s. S\xE9lectionnez \xE0 nouveau.",
197
+ "picker.seatTaken": "Le si\xE8ge {label} vient d'\xEAtre r\xE9serv\xE9 par un autre acheteur.",
198
+ "picker.poweredBy": "Propuls\xE9 par SeatMap",
199
+ "picker.seats.one": "{count} si\xE8ge",
200
+ "picker.seats.other": "{count} si\xE8ges",
201
+ // renderer (drawn on the Konva map — shared by the embed SDK)
202
+ "map.aria": "Plan de salle. Utilisez les fl\xE8ches pour naviguer entre les si\xE8ges, Entr\xE9e pour s\xE9lectionner.",
203
+ "map.seatsLeft": "{count} LIBRES",
204
+ "map.fromPrice": "D\xC8S {price}",
205
+ // buyer picker page (src/pages/PickerPage.tsx)
206
+ "picker.language": "Langue",
207
+ "picker.zoomToFit": "Ajuster le zoom",
208
+ "picker.viewMode": "Mode d'affichage",
209
+ "picker.floor": "\xC9tage",
210
+ "picker.zoomLevel": "Niveau de zoom",
211
+ "picker.rungTip.zones": "Vue la plus large \u2014 blocs de zones (\xC9tage / Parquet inf\xE9rieur / Parquet sup\xE9rieur)",
212
+ "picker.rungTip.sections": "Blocs de sections \u2014 m\xE9lange de cat\xE9gories + teinte de disponibilit\xE9",
213
+ "picker.rungTip.seats": "Si\xE8ges individuels \u2014 les blocs se transforment en points",
214
+ "picker.rungLabel.zones": "ZONES",
215
+ "picker.rungLabel.sections": "SECTIONS",
216
+ "picker.rungLabel.seats": "SI\xC8GES",
217
+ "picker.sectionSummaryAria": "R\xE9sum\xE9 de la section {label}",
218
+ "picker.closeSectionSummary": "Fermer le r\xE9sum\xE9 de la section",
219
+ "picker.seatsLeftInSection.one": "{count} si\xE8ge restant",
220
+ "picker.seatsLeftInSection.other": "{count} si\xE8ges restants",
221
+ "picker.overview": "Vue d'ensemble",
222
+ "picker.tapSeatHint": "Appuyez sur un si\xE8ge pour v\xE9rifier sa vue",
223
+ "picker.chartSize": "Taille du plan",
224
+ "picker.custom": "Personnalis\xE9",
225
+ "picker.categories": "Cat\xE9gories",
226
+ "picker.accessibility": "Accessibilit\xE9",
227
+ "picker.showAnyAccessible": "Afficher un si\xE8ge accessible",
228
+ "picker.any": "N'importe quel",
229
+ "picker.yourSeats": "Vos si\xE8ges",
230
+ "picker.emptySeats": "Appuyez sur les si\xE8ges sur le plan",
231
+ "picker.emptySeatsWithGa": "Appuyez sur les si\xE8ges sur le plan \xB7 appuyez sur une zone debout pour les billets GA",
232
+ "picker.oneFewer": "Un de moins",
233
+ "picker.oneMore": "Un de plus",
234
+ "picker.remove": "Supprimer {label}",
235
+ "picker.ticketTierFor": "Cat\xE9gorie de billet pour {label}",
236
+ "picker.total": "Total : {amount}",
237
+ "picker.viewFromSeat": "Vue depuis le si\xE8ge {label}",
238
+ "picker.real360": "VRAI 360\xB0",
239
+ "picker.preview": "APER\xC7U",
240
+ "picker.open360": "Ouvrir la vue \xE0 360\xB0",
241
+ "picker.sightline": "\u2248 {rows} rang\xE9es de la sc\xE8ne \xB7 {angle}\xB0 d\xE9cal\xE9 du centre",
242
+ "picker.bookedDemo": "R\xE9serv\xE9 ! (d\xE9mo)",
243
+ "picker.bookButton.one": "R\xE9server {count} billet \u2014 {amount}",
244
+ "picker.bookButton.other": "R\xE9server {count} billets \u2014 {amount}",
245
+ "picker.simulateCrowd": "Simuler la foule : {state}",
246
+ "picker.on": "ACTIV\xC9",
247
+ "picker.off": "D\xC9SACTIV\xC9",
248
+ "picker.panorama360": "Photo panoramique 360\xB0 du lieu",
249
+ "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m de la sc\xE8ne"
250
+ };
251
+ }
252
+ });
253
+
20
254
  // src/index.ts
21
255
  var index_exports = {};
22
256
  __export(index_exports, {
23
257
  ACCESSIBILITY_TYPES: () => ACCESSIBILITY_TYPES,
24
258
  CHART_STORAGE_KEY: () => CHART_STORAGE_KEY,
259
+ DEFAULT_CURRENCY: () => DEFAULT_CURRENCY,
25
260
  PickerController: () => PickerController,
261
+ SUPPORTED_LOCALES: () => SUPPORTED_LOCALES,
26
262
  SeatmapRenderer: () => SeatmapRenderer,
263
+ UNGROUPED_ID: () => UNGROUPED_ID,
27
264
  accessibilityMeta: () => accessibilityMeta,
265
+ allObjects: () => allObjects,
266
+ applyHidden: () => applyHidden,
28
267
  chartBounds: () => chartBounds,
268
+ computeSections: () => computeSections,
29
269
  createRenderer: () => createRenderer,
270
+ currencySymbol: () => currencySymbol,
30
271
  expandBooth: () => expandBooth,
31
272
  expandChart: () => expandChart,
32
273
  expandRow: () => expandRow,
33
274
  expandRowSlots: () => expandRowSlots,
34
275
  expandTable: () => expandTable,
276
+ floorObjects: () => floorObjects,
277
+ floorsOf: () => floorsOf,
278
+ formatDate: () => formatDate,
279
+ formatMoney: () => formatMoney,
280
+ getLocale: () => getLocale,
281
+ hiddenObjectIds: () => hiddenObjectIds,
282
+ isSectionHidden: () => isSectionHidden,
35
283
  layerOf: () => layerOf,
284
+ loadLocale: () => loadLocale,
36
285
  objectCenter: () => objectCenter,
37
- pointInPolygon: () => pointInPolygon
286
+ pointInPolygon: () => pointInPolygon,
287
+ resolveLocale: () => resolveLocale,
288
+ setLocale: () => setLocale,
289
+ setMoneyLocale: () => setMoneyLocale,
290
+ setStringOverrides: () => setStringOverrides,
291
+ stackFloors: () => stackFloors,
292
+ t: () => t,
293
+ tCount: () => tCount
38
294
  });
39
295
  module.exports = __toCommonJS(index_exports);
40
296
 
@@ -118,7 +374,15 @@ function expandRowSlots(row) {
118
374
  const start = row.seatLabelStart ?? 1;
119
375
  const dir = row.seatNumbering?.direction ?? "ltr";
120
376
  const step = row.seatNumbering?.step ?? 1;
121
- const seatNumber = (i) => start + (dir === "rtl" ? row.seatCount - 1 - i : i) * step;
377
+ const n = row.seatCount;
378
+ let seatNumber;
379
+ if (dir === "center") {
380
+ const rank = new Array(n);
381
+ 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);
382
+ seatNumber = (i) => start + rank[i] * step;
383
+ } else {
384
+ seatNumber = (i) => start + (dir === "rtl" ? n - 1 - i : i) * step;
385
+ }
122
386
  const ov = overrideMap(row);
123
387
  return rowSeatPositions(row).map((p, i) => {
124
388
  const o = ov.get(i);
@@ -153,30 +417,41 @@ function expandRow(row) {
153
417
  }
154
418
  return seats;
155
419
  }
156
- function expandTable(t) {
420
+ function expandTable(t2) {
157
421
  const seats = [];
158
- const n = Math.max(0, Math.round(t.seatCount));
422
+ const n = Math.max(0, Math.round(t2.seatCount));
159
423
  if (n === 0) return seats;
160
424
  const mk = (i, x, y) => ({
161
- id: `${t.id}:${i}`,
162
- label: `${t.label}-${i + 1}`,
425
+ id: `${t2.id}:${i}`,
426
+ label: `${t2.label}-${i + 1}`,
163
427
  x,
164
428
  y,
165
- rowId: t.id,
166
- categoryKey: t.categoryKey
429
+ rowId: t2.id,
430
+ categoryKey: t2.categoryKey
167
431
  });
168
- if (t.shape === "round") {
169
- const R = (t.radius ?? 40) + TABLE_SEAT_OFFSET;
170
- const base = t.rotation * DEG;
432
+ if (t2.shape === "round") {
433
+ const R = (t2.radius ?? 40) + TABLE_SEAT_OFFSET;
434
+ const base = t2.rotation * DEG;
435
+ const arc = Math.max(0, Math.min(360, t2.seatArc ?? 360));
436
+ if (arc >= 360 || n === 1) {
437
+ for (let i = 0; i < n; i++) {
438
+ const a = base + i / n * 2 * Math.PI;
439
+ seats.push(mk(i, t2.center.x + R * Math.cos(a), t2.center.y + R * Math.sin(a)));
440
+ }
441
+ return seats;
442
+ }
443
+ const arcRad = arc * DEG;
444
+ const halfGap = (2 * Math.PI - arcRad) / 2;
445
+ const start = base + halfGap;
171
446
  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)));
447
+ const a = start + i / (n - 1) * arcRad;
448
+ seats.push(mk(i, t2.center.x + R * Math.cos(a), t2.center.y + R * Math.sin(a)));
174
449
  }
175
450
  return seats;
176
451
  }
177
- const w = t.width ?? 80;
178
- const h = t.height ?? 50;
179
- const enabled = t.sides && t.sides.length ? t.sides : ["top", "bottom"];
452
+ const w = t2.width ?? 80;
453
+ const h = t2.height ?? 50;
454
+ const enabled = t2.sides && t2.sides.length ? t2.sides : ["top", "bottom"];
180
455
  const order = ["top", "bottom", "left", "right"].filter((s) => enabled.includes(s));
181
456
  if (!order.length) return seats;
182
457
  const counts = new Map(order.map((s) => [s, 0]));
@@ -203,7 +478,7 @@ function expandTable(t) {
203
478
  localX = w / 2 + TABLE_SEAT_OFFSET;
204
479
  localY = -h / 2 + (j + 0.5) * h / count;
205
480
  }
206
- const p = place(localX, localY, t.rotation, t.center);
481
+ const p = place(localX, localY, t2.rotation, t2.center);
207
482
  seats.push(mk(idx++, p.x, p.y));
208
483
  }
209
484
  }
@@ -274,9 +549,53 @@ function objectCenter(o) {
274
549
  return { x: o.x ?? 0, y: o.y ?? 0 };
275
550
  }
276
551
  }
552
+ function floorsOf(doc) {
553
+ if (doc.floors && doc.floors.length) return doc.floors;
554
+ return [{ id: "floor-0", name: "Main", objects: doc.objects, focalPoint: doc.focalPoint, backgroundImage: doc.backgroundImage }];
555
+ }
556
+ function floorObjects(doc, floorId) {
557
+ const floors = floorsOf(doc);
558
+ return (floorId ? floors.find((f) => f.id === floorId) : floors[0])?.objects ?? [];
559
+ }
560
+ function allObjects(doc) {
561
+ return doc.floors && doc.floors.length ? doc.floors.flatMap((f) => f.objects) : doc.objects;
562
+ }
563
+ function translateObject(o, dx, dy) {
564
+ const p = (pt) => ({ x: pt.x + dx, y: pt.y + dy });
565
+ const pts = (a) => a.map(p);
566
+ switch (o.type) {
567
+ case "row":
568
+ return { ...o, origin: p(o.origin) };
569
+ case "table":
570
+ case "booth":
571
+ return { ...o, center: p(o.center) };
572
+ case "gaArea":
573
+ return { ...o, points: pts(o.points) };
574
+ case "section":
575
+ return { ...o, outline: pts(o.outline) };
576
+ case "text":
577
+ return { ...o, position: p(o.position) };
578
+ case "shape":
579
+ return {
580
+ ...o,
581
+ ...o.points ? { points: pts(o.points) } : {},
582
+ ...o.x != null ? { x: o.x + dx } : {},
583
+ ...o.y != null ? { y: o.y + dy } : {}
584
+ };
585
+ }
586
+ }
587
+ function stackFloors(doc, spread = 900) {
588
+ if (!doc.floors || doc.floors.length < 2) return doc;
589
+ const objects = [];
590
+ doc.floors.forEach((f, i) => {
591
+ const dy = -i * spread;
592
+ for (const o of f.objects) objects.push(dy === 0 ? o : translateObject(o, 0, dy));
593
+ });
594
+ return { ...doc, objects, floors: void 0 };
595
+ }
277
596
  function expandChart(doc) {
278
597
  const out = [];
279
- for (const obj of doc.objects) {
598
+ for (const obj of allObjects(doc)) {
280
599
  if (obj.type === "row") out.push(...expandRow(obj));
281
600
  else if (obj.type === "table") out.push(...expandTable(obj));
282
601
  else if (obj.type === "booth") out.push(...expandBooth(obj));
@@ -296,7 +615,7 @@ function chartBounds(doc) {
296
615
  if (y > maxY) maxY = y;
297
616
  };
298
617
  for (const s of expandChart(doc)) acc(s.x, s.y);
299
- for (const obj of doc.objects) {
618
+ for (const obj of allObjects(doc)) {
300
619
  if (obj.type === "gaArea") {
301
620
  for (const p of obj.points) acc(p.x, p.y);
302
621
  } else if (obj.type === "section") {
@@ -341,6 +660,87 @@ function chartBounds(doc) {
341
660
  };
342
661
  }
343
662
 
663
+ // src/core/sections.ts
664
+ var UNGROUPED_ID = "__ungrouped__";
665
+ function objectSeatLabels(o) {
666
+ if (o.type === "row") return expandRow(o).map((s) => s.label);
667
+ if (o.type === "table") return expandTable(o).map((s) => s.label);
668
+ if (o.type === "booth") return expandBooth(o).map((s) => s.label);
669
+ return [];
670
+ }
671
+ function isSeatObject(o) {
672
+ return o.type === "row" || o.type === "table" || o.type === "booth";
673
+ }
674
+ function computeSections(doc) {
675
+ const objs = allObjects(doc);
676
+ const sectionObjs = objs.filter((o) => o.type === "section");
677
+ const nodes = /* @__PURE__ */ new Map();
678
+ for (const s of sectionObjs) {
679
+ nodes.set(s.id, { id: s.id, label: s.label || "Section", zone: s.zone, seatCount: 0, objectIds: [], seatLabels: [] });
680
+ }
681
+ const ungrouped = { id: UNGROUPED_ID, label: "Other seats", seatCount: 0, objectIds: [], seatLabels: [] };
682
+ const objectToSection = /* @__PURE__ */ new Map();
683
+ for (const obj of objs) {
684
+ if (!isSeatObject(obj)) continue;
685
+ const labels = objectSeatLabels(obj);
686
+ if (labels.length === 0) continue;
687
+ const c = objectCenter(obj);
688
+ const owner = sectionObjs.find((s) => pointInPolygon(c, s.outline));
689
+ const node = owner ? nodes.get(owner.id) : ungrouped;
690
+ node.seatCount += labels.length;
691
+ node.objectIds.push(obj.id);
692
+ node.seatLabels.push(...labels);
693
+ objectToSection.set(obj.id, node.id);
694
+ }
695
+ return {
696
+ sections: sectionObjs.map((s) => nodes.get(s.id)),
697
+ ungrouped: ungrouped.objectIds.length ? ungrouped : null,
698
+ objectToSection
699
+ };
700
+ }
701
+ function isSectionHidden(s, hidden) {
702
+ return hidden.has(s.id) || !!s.zone && hidden.has(s.zone);
703
+ }
704
+ function hiddenObjectIds(doc, hidden) {
705
+ const out = /* @__PURE__ */ new Set();
706
+ if (!hidden.size) return out;
707
+ const { sections, ungrouped, objectToSection } = computeSections(doc);
708
+ const hiddenSectionIds = /* @__PURE__ */ new Set();
709
+ const zoneById = new Map(sections.map((s) => [s.id, s.zone]));
710
+ for (const s of sections) {
711
+ const zone = zoneById.get(s.id);
712
+ if (hidden.has(s.id) || zone && hidden.has(zone)) hiddenSectionIds.add(s.id);
713
+ }
714
+ const ungroupedHidden = !!ungrouped && hidden.has(UNGROUPED_ID);
715
+ for (const [objId, secId] of objectToSection) {
716
+ if (hiddenSectionIds.has(secId) || ungroupedHidden && secId === UNGROUPED_ID) out.add(objId);
717
+ }
718
+ return out;
719
+ }
720
+ function applyHidden(doc, hidden) {
721
+ if (!hidden.size) return doc;
722
+ const hide = hiddenObjectIds(doc, hidden);
723
+ const hiddenSet = hidden instanceof Set ? hidden : new Set(hidden);
724
+ const keep = (o) => {
725
+ if (hide.has(o.id)) return false;
726
+ if (o.type === "section" && isSectionHidden(o, hiddenSet)) return false;
727
+ return true;
728
+ };
729
+ if (doc.floors && doc.floors.length) {
730
+ let changed = false;
731
+ const floors = doc.floors.map((f) => {
732
+ const objects2 = f.objects.filter(keep);
733
+ if (objects2.length !== f.objects.length) changed = true;
734
+ return objects2.length === f.objects.length ? f : { ...f, objects: objects2 };
735
+ });
736
+ if (!changed) return doc;
737
+ return { ...doc, floors, objects: floors[0].objects };
738
+ }
739
+ const objects = doc.objects.filter(keep);
740
+ if (objects.length === doc.objects.length) return doc;
741
+ return { ...doc, objects };
742
+ }
743
+
344
744
  // src/engine/SeatmapRenderer.ts
345
745
  var import_Core = require("konva/lib/Core");
346
746
  var import_Stage = require("konva/lib/Stage");
@@ -352,21 +752,162 @@ var import_Ellipse = require("konva/lib/shapes/Ellipse");
352
752
  var import_Line = require("konva/lib/shapes/Line");
353
753
  var import_Text = require("konva/lib/shapes/Text");
354
754
  var import_Image = require("konva/lib/shapes/Image");
755
+
756
+ // src/lib/money.ts
757
+ var DEFAULT_CURRENCY = "USD";
758
+ var displayLocale;
759
+ function setMoneyLocale(locale) {
760
+ displayLocale = locale;
761
+ }
762
+ var formatterCache = /* @__PURE__ */ new Map();
763
+ function formatter(currency, digits) {
764
+ const key = `${displayLocale ?? ""}|${currency}|${digits ?? "auto"}`;
765
+ let fmt = formatterCache.get(key);
766
+ if (!fmt) {
767
+ fmt = new Intl.NumberFormat(displayLocale, {
768
+ style: "currency",
769
+ currency,
770
+ minimumFractionDigits: digits,
771
+ maximumFractionDigits: digits
772
+ });
773
+ formatterCache.set(key, fmt);
774
+ }
775
+ return fmt;
776
+ }
777
+ function formatMoney(amount, currency = DEFAULT_CURRENCY, fractionDigits) {
778
+ const digits = fractionDigits ?? (Number.isInteger(amount) ? 0 : void 0);
779
+ return formatter(currency, digits).format(amount);
780
+ }
781
+ function currencySymbol(currency = DEFAULT_CURRENCY) {
782
+ const part = formatter(currency, 0).formatToParts(0).find((p) => p.type === "currency");
783
+ return part?.value ?? currency;
784
+ }
785
+
786
+ // src/i18n/locales/en.ts
787
+ var en = {
788
+ // shared
789
+ "common.cancel": "Cancel",
790
+ "common.close": "Close",
791
+ "common.done": "Done",
792
+ "common.copied": "\u2713 Copied",
793
+ // buyer picker (extraction in progress — P4 of the port plan)
794
+ "picker.holdSeats": "Hold seats & checkout",
795
+ "picker.completeBooking": "Complete booking",
796
+ "picker.seatsHeld": "Seats held \u2014 {time}",
797
+ "picker.holdExpired": "Your hold expired \u2014 the seats were released. Pick again.",
798
+ "picker.seatTaken": "Seat {label} was just taken by another buyer.",
799
+ "picker.poweredBy": "Powered by SeatMap",
800
+ "picker.seats.one": "{count} seat",
801
+ "picker.seats.other": "{count} seats",
802
+ // renderer (drawn on the Konva map — shared by the embed SDK)
803
+ "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
804
+ "map.seatsLeft": "{count} LEFT",
805
+ "map.fromPrice": "FROM {price}",
806
+ // buyer picker page (src/pages/PickerPage.tsx)
807
+ "picker.language": "Language",
808
+ "picker.zoomToFit": "Zoom to fit",
809
+ "picker.viewMode": "View mode",
810
+ "picker.floor": "Floor",
811
+ "picker.zoomLevel": "Zoom level",
812
+ "picker.rungTip.zones": "Farthest rung \u2014 zone blocks (Floor / Lower Bowl / Upper Bowl)",
813
+ "picker.rungTip.sections": "Section blocks \u2014 category-mix fill + availability tint",
814
+ "picker.rungTip.seats": "Individual seats \u2014 blocks melt into dots",
815
+ "picker.rungLabel.zones": "ZONES",
816
+ "picker.rungLabel.sections": "SECTIONS",
817
+ "picker.rungLabel.seats": "SEATS",
818
+ "picker.sectionSummaryAria": "{label} section summary",
819
+ "picker.closeSectionSummary": "Close section summary",
820
+ "picker.seatsLeftInSection.one": "{count} seat left",
821
+ "picker.seatsLeftInSection.other": "{count} seats left",
822
+ "picker.overview": "Overview",
823
+ "picker.tapSeatHint": "Tap any seat to check its view",
824
+ "picker.chartSize": "Chart size",
825
+ "picker.custom": "Custom",
826
+ "picker.categories": "Categories",
827
+ "picker.accessibility": "Accessibility",
828
+ "picker.showAnyAccessible": "Show any accessible seat",
829
+ "picker.any": "Any",
830
+ "picker.yourSeats": "Your seats",
831
+ "picker.emptySeats": "Tap seats on the map",
832
+ "picker.emptySeatsWithGa": "Tap seats on the map \xB7 tap a standing area for GA tickets",
833
+ "picker.oneFewer": "One fewer",
834
+ "picker.oneMore": "One more",
835
+ "picker.remove": "Remove {label}",
836
+ "picker.ticketTierFor": "Ticket tier for {label}",
837
+ "picker.total": "Total: {amount}",
838
+ "picker.viewFromSeat": "View from seat {label}",
839
+ "picker.real360": "REAL 360\xB0",
840
+ "picker.preview": "PREVIEW",
841
+ "picker.open360": "Open 360\xB0 view",
842
+ "picker.sightline": "\u2248 {rows} rows from stage \xB7 {angle}\xB0 off-center",
843
+ "picker.bookedDemo": "Booked! (demo)",
844
+ "picker.bookButton.one": "Book {count} ticket \u2014 {amount}",
845
+ "picker.bookButton.other": "Book {count} tickets \u2014 {amount}",
846
+ "picker.simulateCrowd": "Simulate crowd: {state}",
847
+ "picker.on": "ON",
848
+ "picker.off": "OFF",
849
+ "picker.panorama360": "360\xB0 venue photo",
850
+ "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m from stage"
851
+ };
852
+
853
+ // src/i18n/index.ts
854
+ var SUPPORTED_LOCALES = ["en", "es", "de", "fr"];
855
+ var bundles = { en };
856
+ var overrides = {};
857
+ var active = "en";
858
+ function resolveLocale(explicit, stored) {
859
+ for (const candidate of [explicit, stored, typeof navigator !== "undefined" ? navigator.language : null]) {
860
+ if (!candidate) continue;
861
+ const base = candidate.toLowerCase().split("-")[0];
862
+ if (SUPPORTED_LOCALES.includes(base)) return base;
863
+ }
864
+ return "en";
865
+ }
866
+ function getLocale() {
867
+ return active;
868
+ }
869
+ function setLocale(locale, bundle) {
870
+ if (bundle) bundles[locale] = { ...bundles[locale], ...bundle };
871
+ active = bundles[locale] ? locale : "en";
872
+ setMoneyLocale(active);
873
+ if (typeof document !== "undefined") document.documentElement.lang = active;
874
+ }
875
+ function setStringOverrides(next) {
876
+ overrides = next;
877
+ }
878
+ var PLACEHOLDER = /\{(\w+)\}/g;
879
+ function t(key, vars) {
880
+ const raw = overrides[key] ?? bundles[active]?.[key] ?? en[key] ?? key;
881
+ if (!vars) return raw;
882
+ return raw.replace(PLACEHOLDER, (_, name) => String(vars[name] ?? `{${name}}`));
883
+ }
884
+ function tCount(key, count, vars) {
885
+ const rule = new Intl.PluralRules(active).select(count);
886
+ const exact = overrides[`${key}.${rule}`] ?? bundles[active]?.[`${key}.${rule}`] ?? en[`${key}.${rule}`];
887
+ const raw = exact ?? overrides[`${key}.other`] ?? bundles[active]?.[`${key}.other`] ?? en[`${key}.other`] ?? key;
888
+ return raw.replace(PLACEHOLDER, (_, name) => String({ count, ...vars }[name] ?? `{${name}}`));
889
+ }
890
+ function formatDate(value, opts) {
891
+ return new Intl.DateTimeFormat(active, opts ?? { dateStyle: "medium", timeStyle: "short" }).format(value);
892
+ }
893
+
894
+ // src/engine/SeatmapRenderer.ts
355
895
  var SEAT_RADIUS = 9;
356
896
  var SEAT_LEGIBLE_SCALE = 0.9;
357
897
  var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
358
898
  var LABEL_SCALE = 1;
359
- var SECTION_PROMINENT_SCALE = 0.35 * SEAT_LEGIBLE_SCALE;
899
+ var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
900
+ var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
360
901
  var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
361
902
  var MAX_LABELS = 700;
362
903
  var ISO_ANGLE_DEG = -11.5;
363
904
  var ISO_SQUASH = 0.58;
364
- var LIFT_PER_STEP = 46;
905
+ var LIFT_PER_STEP = 58;
365
906
  var ISO_TWEEN_MS = 320;
366
907
  var BLOCK_FILL_ALPHA = 0.85;
367
908
  var SOLD_DARKEN = 0.5;
368
- var SECTION_LABEL_PX = 18;
369
- var SECTION_SUB_PX = 11;
909
+ var SECTION_LABEL_PX = 20;
910
+ var SECTION_SUB_PX = 12.5;
370
911
  var ZONE_LABEL_PX = 30;
371
912
  var ZONE_SUB_PX = 12;
372
913
  var HELD_FILL = "#6b7280";
@@ -383,7 +924,7 @@ var ACCESS_RING = {
383
924
  };
384
925
  function seatMatchesAccess(seat, filter) {
385
926
  if (filter.length === 0) return !!seat.accessible;
386
- return !!seat.accessibility?.some((t) => filter.includes(t));
927
+ return !!seat.accessibility?.some((t2) => filter.includes(t2));
387
928
  }
388
929
  var DEF_SEAT_LABEL = "#0b1220";
389
930
  var DEF_SELECTION = "#ffffff";
@@ -452,16 +993,21 @@ function mixColors(parts, fallback) {
452
993
  }
453
994
  return tw > 0 ? toHex(r / tw, g / tw, b / tw) : fallback;
454
995
  }
455
- function lerpColor(a, b, t) {
996
+ function lerpColor(a, b, t2) {
456
997
  const ca = hexToRgb(a);
457
998
  const cb = hexToRgb(b);
458
999
  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);
1000
+ return toHex(ca[0] + (cb[0] - ca[0]) * t2, ca[1] + (cb[1] - ca[1]) * t2, ca[2] + (cb[2] - ca[2]) * t2);
460
1001
  }
461
1002
  var _SeatmapRenderer = class _SeatmapRenderer {
462
1003
  constructor(container, options = {}) {
463
1004
  this.seats = [];
464
1005
  this.seatById = /* @__PURE__ */ new Map();
1006
+ /** Multi-floor (Batch 5): the last-set chart + which floor we're rendering. */
1007
+ this.chartDoc = null;
1008
+ this.activeFloorId = "";
1009
+ /** When true on a multi-floor chart, render ALL floors stacked (3D overview). */
1010
+ this.stacked = false;
465
1011
  /** Interactive node per seat/booth — a Circle for seats, a Rect for booths. */
466
1012
  this.circleById = /* @__PURE__ */ new Map();
467
1013
  /** Booth block geometry, keyed by booth id (= the unit's rowId). */
@@ -485,6 +1031,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
485
1031
  this.zones = [];
486
1032
  this.seatSection = /* @__PURE__ */ new Map();
487
1033
  this.catPrice = /* @__PURE__ */ new Map();
1034
+ /** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
1035
+ this.dimmedSections = /* @__PURE__ */ new Set();
1036
+ /** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
1037
+ this.objectFloor = /* @__PURE__ */ new Map();
488
1038
  /** Zone id → colour (drives extruded side faces in iso view). */
489
1039
  this.zoneColor = /* @__PURE__ */ new Map();
490
1040
  this.hasSections = false;
@@ -497,6 +1047,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
497
1047
  this.isoRaf = 0;
498
1048
  /** Chart centre the iso projection pivots about (bounds centre). */
499
1049
  this.isoCentre = { x: 0, y: 0 };
1050
+ /** rAF for an in-flight camera glide (focusRegion / setRung); 0 = none. */
1051
+ this.glideRaf = 0;
500
1052
  /** Set in destroy() so an in-flight iso tween bails. */
501
1053
  this.destroyed = false;
502
1054
  /** Cached scale the section/zone labels were last sized for (scale-compensation). */
@@ -544,6 +1096,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
544
1096
  // pointer events away from Konva's canvas, killing its click/tap/hover
545
1097
  // pipeline. We rely on bubbling instead.
546
1098
  this.onPointerDown = (e) => {
1099
+ this.cancelGlide();
547
1100
  this.pointers.set(e.pointerId, this.toLocal(e));
548
1101
  if (this.pointers.size === 1) {
549
1102
  this.moved = 0;
@@ -605,6 +1158,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
605
1158
  };
606
1159
  this.container = container;
607
1160
  this.opts = { maxSelection: 10, selectableStatuses: ["free"], ...options };
1161
+ this.currency = options.currency;
608
1162
  import_Core.Konva.pixelRatio = this.dpr;
609
1163
  this.stage = new import_Stage.Stage({
610
1164
  container,
@@ -621,7 +1175,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
621
1175
  if (container.tabIndex < 0) container.tabIndex = 0;
622
1176
  container.setAttribute("role", "application");
623
1177
  if (!container.getAttribute("aria-label")) {
624
- container.setAttribute("aria-label", "Seating map. Use arrow keys to move between seats, Enter to select.");
1178
+ container.setAttribute("aria-label", t("map.aria"));
625
1179
  }
626
1180
  container.addEventListener("keydown", this.onKeyDown);
627
1181
  this.bgLayer = new import_Layer.Layer({ listening: true });
@@ -664,7 +1218,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
664
1218
  }
665
1219
  }
666
1220
  // ---- ISeatmapRenderer -----------------------------------------------------
667
- setChart(doc) {
1221
+ setChart(doc, opts) {
1222
+ if (doc !== this.chartDoc) this.stacked = false;
1223
+ this.chartDoc = doc;
1224
+ this.activeFloorId = opts?.floorId ?? floorsOf(doc)[0].id;
1225
+ this.objectFloor.clear();
1226
+ if (doc.floors && doc.floors.length > 1) {
1227
+ for (const f of doc.floors) for (const o of f.objects) this.objectFloor.set(o.id, f.id);
1228
+ }
1229
+ const view = this.floorView(doc);
668
1230
  this.focusedId = null;
669
1231
  this.focusRing.visible(false);
670
1232
  this.bgLayer.destroyChildren();
@@ -692,7 +1254,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
692
1254
  this.isoT = 0;
693
1255
  this.isoTarget = 0;
694
1256
  this.resetLayerTransforms();
695
- this.hasSections = doc.objects.some((o) => o.type === "section");
1257
+ this.hasSections = view.objects.some((o) => o.type === "section");
696
1258
  for (const z of doc.zones ?? []) if (z.color) this.zoneColor.set(z.id, z.color);
697
1259
  this.seatLayer.opacity(1);
698
1260
  this.hoverRing.visible(false);
@@ -707,24 +1269,56 @@ var _SeatmapRenderer = class _SeatmapRenderer {
707
1269
  this.catColor.set(c.key, c.color);
708
1270
  if (typeof c.price === "number") this.catPrice.set(c.key, c.price);
709
1271
  }
710
- for (const obj of doc.objects) {
1272
+ for (const obj of view.objects) {
711
1273
  if (obj.type === "booth") {
712
1274
  this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
713
1275
  }
714
1276
  }
715
- this.seats = expandChart(doc);
1277
+ this.seats = expandChart(view);
716
1278
  for (const s of this.seats) {
717
1279
  this.seatById.set(s.id, s);
718
1280
  this.statusById.set(s.id, "free");
719
1281
  }
720
- this.renderBackground(doc);
1282
+ this.renderBackground(view);
721
1283
  this.renderSeats();
722
1284
  this.overlayLayer.add(this.labelGroup);
723
1285
  this.overlayLayer.add(this.hoverRing);
724
- this.bounds = chartBounds(doc);
1286
+ this.bounds = chartBounds(view);
725
1287
  this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
726
1288
  this.zoomToFit();
727
1289
  }
1290
+ /** The chart to render: all floors stacked (3D overview), the active floor, or
1291
+ * the whole chart for single-floor charts. */
1292
+ floorView(doc) {
1293
+ if (!doc.floors || !doc.floors.length) return doc;
1294
+ if (this.stacked && doc.floors.length >= 2) return stackFloors(doc);
1295
+ const floor = doc.floors.find((f) => f.id === this.activeFloorId) ?? doc.floors[0];
1296
+ return { ...doc, objects: floor.objects, focalPoint: floor.focalPoint, backgroundImage: floor.backgroundImage, floors: void 0 };
1297
+ }
1298
+ /** Toggle the 3D all-floors stacked overview (Batch 5). Re-renders; no-op on
1299
+ * single-floor charts. The caller re-applies statuses + animates the iso view. */
1300
+ setStacked(on) {
1301
+ if (!this.chartDoc || on === this.stacked) return;
1302
+ const multi = !!this.chartDoc.floors && this.chartDoc.floors.length >= 2;
1303
+ if (!multi) return;
1304
+ this.stacked = on;
1305
+ this.setChart(this.chartDoc, { floorId: this.activeFloorId });
1306
+ }
1307
+ isStacked() {
1308
+ return this.stacked;
1309
+ }
1310
+ /** Switch which floor is shown (2D). Re-renders + re-fits; no-op if unchanged. */
1311
+ setActiveFloor(floorId) {
1312
+ if (!this.chartDoc || floorId === this.activeFloorId) return;
1313
+ this.setChart(this.chartDoc, { floorId });
1314
+ }
1315
+ /** Floors for the host's switcher (single-floor charts return one synthetic floor). */
1316
+ getFloors() {
1317
+ return this.chartDoc ? floorsOf(this.chartDoc).map((f) => ({ id: f.id, name: f.name })) : [];
1318
+ }
1319
+ getActiveFloorId() {
1320
+ return this.activeFloorId;
1321
+ }
728
1322
  setStatus(seatIds, status) {
729
1323
  let touched = false;
730
1324
  const affected = this.hasSections ? /* @__PURE__ */ new Set() : null;
@@ -804,11 +1398,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
804
1398
  const dur = 620;
805
1399
  const step = (now) => {
806
1400
  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));
1401
+ const t2 = Math.min(1, (now - start) / dur);
1402
+ ring.radius(this.seatR * (1 + t2 * 1.8));
1403
+ ring.opacity(0.9 * (1 - t2));
810
1404
  this.overlayLayer.batchDraw();
811
- if (t < 1) requestAnimationFrame(step);
1405
+ if (t2 < 1) requestAnimationFrame(step);
812
1406
  else {
813
1407
  ring.destroy();
814
1408
  this.overlayLayer.batchDraw();
@@ -915,7 +1509,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
915
1509
  sameAccessFilter(next) {
916
1510
  const cur = this.accessFilter;
917
1511
  if (cur === null || next === null) return cur === next;
918
- return cur.length === next.length && cur.every((t, i) => t === next[i]);
1512
+ return cur.length === next.length && cur.every((t2, i) => t2 === next[i]);
919
1513
  }
920
1514
  // ---- isometric ("3D") view mode -------------------------------------------
921
1515
  /**
@@ -963,6 +1557,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
963
1557
  };
964
1558
  this.isoRaf = requestAnimationFrame(step);
965
1559
  }
1560
+ /** Current projection — reflects the tween target, not the mid-tween isoT. */
1561
+ getViewMode() {
1562
+ return this.isoTarget === 1 ? "isometric" : "flat";
1563
+ }
966
1564
  /** Iso angle (rad) + y-squash for the current isoT. */
967
1565
  isoParams() {
968
1566
  return { th: ISO_ANGLE_DEG * Math.PI / 180 * this.isoT, sg: 1 - (1 - ISO_SQUASH) * this.isoT };
@@ -1020,8 +1618,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1020
1618
  * lift + extruded side faces on elevated sections.
1021
1619
  */
1022
1620
  applyIso() {
1023
- const t = this.isoT;
1024
- if (t === 0) {
1621
+ const t2 = this.isoT;
1622
+ if (t2 === 0) {
1025
1623
  this.resetLayerTransforms();
1026
1624
  } else {
1027
1625
  const { th, sg } = this.isoParams();
@@ -1074,13 +1672,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1074
1672
  }
1075
1673
  /** Lift elevated sections (+ their members) and update their extruded side faces. */
1076
1674
  applyElevation() {
1077
- const t = this.isoT;
1675
+ const t2 = this.isoT;
1078
1676
  for (const sec of this.sections) {
1079
1677
  if (sec.elevation <= 0) continue;
1080
1678
  const off = this.isoLiftLocal(sec.elevation);
1081
1679
  sec.liftGroupBg?.position(off);
1082
1680
  sec.liftGroupSeat?.position(off);
1083
- const alpha = 0.9 * t;
1681
+ const alpha = 0.9 * t2;
1084
1682
  for (let i = 0; i < sec.sideFaces.length; i++) {
1085
1683
  const face = sec.sideFaces[i];
1086
1684
  const p0 = sec.outline[i];
@@ -1094,6 +1692,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1094
1692
  this.destroyed = true;
1095
1693
  if (this.rafId) cancelAnimationFrame(this.rafId);
1096
1694
  if (this.isoRaf) cancelAnimationFrame(this.isoRaf);
1695
+ if (this.glideRaf) cancelAnimationFrame(this.glideRaf);
1097
1696
  if (this.viewChangeRaf) cancelAnimationFrame(this.viewChangeRaf);
1098
1697
  if (this.recacheTimer) clearTimeout(this.recacheTimer);
1099
1698
  this.resizeObs?.disconnect();
@@ -1173,7 +1772,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1173
1772
  this.circleById.set(seat.id, rect);
1174
1773
  this.paintSeat(rect, seat.id);
1175
1774
  target.add(rect);
1176
- const t = new import_Text.Text({
1775
+ const t2 = new import_Text.Text({
1177
1776
  x: seat.x,
1178
1777
  y: seat.y,
1179
1778
  text: seat.label,
@@ -1184,10 +1783,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1184
1783
  listening: false,
1185
1784
  perfectDrawEnabled: false
1186
1785
  });
1187
- t.offsetX(t.width() / 2);
1188
- t.offsetY(t.height() / 2);
1786
+ t2.offsetX(t2.width() / 2);
1787
+ t2.offsetY(t2.height() / 2);
1189
1788
  this.hasBoothText = true;
1190
- target.add(t);
1789
+ target.add(t2);
1191
1790
  }
1192
1791
  /** Apply fill/stroke/opacity for a seat's current status + selection. */
1193
1792
  paintSeat(c, id) {
@@ -1223,6 +1822,32 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1223
1822
  if (this.categoryHighlight && status === "free" && !selected && seat.categoryKey !== this.categoryHighlight) {
1224
1823
  c.opacity(0.25);
1225
1824
  }
1825
+ if (this.dimmedSections.size) {
1826
+ const sec = this.seatSection.get(id);
1827
+ if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
1828
+ c.opacity(0.18);
1829
+ }
1830
+ }
1831
+ }
1832
+ /**
1833
+ * Dim the seats of these section/zone ids (organizer manager use) so held-back
1834
+ * inventory is visually distinct on the canvas without hiding it. `null`/empty
1835
+ * clears. Unlike the buyer's applyHidden, the sections stay rendered.
1836
+ */
1837
+ setDimmedSections(ids) {
1838
+ const next = new Set(ids ?? []);
1839
+ if (next.size === this.dimmedSections.size && [...next].every((i) => this.dimmedSections.has(i))) return;
1840
+ this.dimmedSections = next;
1841
+ for (const seat of this.seats) {
1842
+ const c = this.circleById.get(seat.id);
1843
+ if (c) this.paintSeat(c, seat.id);
1844
+ }
1845
+ if (this.cached) {
1846
+ this.seatLayer.clearCache();
1847
+ this.cacheSeatLayer();
1848
+ } else {
1849
+ this.seatLayer.batchDraw();
1850
+ }
1226
1851
  }
1227
1852
  /** Legend hover: highlight one category (dim the rest), or null to clear. */
1228
1853
  setCategoryHighlight(key) {
@@ -1405,7 +2030,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1405
2030
  }
1406
2031
  /** Prominent stage caption: uppercase, letter-spaced, larger, softly dimmed. */
1407
2032
  addStageLabel(x, y, text) {
1408
- const t = new import_Text.Text({
2033
+ const t2 = new import_Text.Text({
1409
2034
  x,
1410
2035
  y,
1411
2036
  text: text.toUpperCase(),
@@ -1417,9 +2042,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1417
2042
  listening: false,
1418
2043
  perfectDrawEnabled: false
1419
2044
  });
1420
- t.offsetX(t.width() / 2);
1421
- t.offsetY(t.height() / 2);
1422
- this.bgLayer.add(t);
2045
+ t2.offsetX(t2.width() / 2);
2046
+ t2.offsetY(t2.height() / 2);
2047
+ this.bgLayer.add(t2);
1423
2048
  }
1424
2049
  renderGA(obj) {
1425
2050
  const color = this.catColor.get(obj.categoryKey) ?? "#6e7bff";
@@ -1500,12 +2125,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1500
2125
  this.seatLayer.add(liftGroupSeat);
1501
2126
  }
1502
2127
  const bgTarget = liftGroupBg ?? this.bgLayer;
2128
+ const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
1503
2129
  const outlinePoly = new import_Line.Line({
1504
2130
  points: pts,
1505
2131
  closed: true,
1506
- stroke: "#3a4358",
1507
- strokeWidth: 1.5,
1508
- fill: rgba(obj.color ?? "#3a4358", 0.06),
2132
+ stroke: rgba(outlineTint, 0.5),
2133
+ strokeWidth: 1.75,
2134
+ fill: rgba(outlineTint, 0.08),
2135
+ lineJoin: "round",
1509
2136
  listening: false,
1510
2137
  perfectDrawEnabled: false
1511
2138
  });
@@ -1521,6 +2148,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1521
2148
  perfectDrawEnabled: false
1522
2149
  });
1523
2150
  bgTarget.add(blockPoly);
2151
+ const rowSeats = /* @__PURE__ */ new Map();
2152
+ for (const id of memberIds) {
2153
+ const s = this.seatById.get(id);
2154
+ if (!s) continue;
2155
+ const i = Number(id.slice(id.lastIndexOf(":") + 1)) || 0;
2156
+ (rowSeats.get(s.rowId) ?? rowSeats.set(s.rowId, []).get(s.rowId)).push({ i, x: s.x, y: s.y });
2157
+ }
2158
+ const rowLines = [];
2159
+ for (const arr of rowSeats.values()) {
2160
+ if (arr.length < 2) continue;
2161
+ arr.sort((a, b) => a.i - b.i);
2162
+ const line = new import_Line.Line({
2163
+ points: arr.flatMap((p) => [p.x, p.y]),
2164
+ stroke: rgba("#ffffff", 0.34),
2165
+ strokeWidth: SEAT_RADIUS * 0.55,
2166
+ lineCap: "round",
2167
+ lineJoin: "round",
2168
+ opacity: 0,
2169
+ listening: false,
2170
+ perfectDrawEnabled: false
2171
+ });
2172
+ rowLines.push(line);
2173
+ bgTarget.add(line);
2174
+ }
1524
2175
  const nameLabel = new import_Text.Text({
1525
2176
  x: centroid.x,
1526
2177
  y: centroid.y,
@@ -1529,6 +2180,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1529
2180
  fontStyle: "700",
1530
2181
  fontFamily: this.labelFont(),
1531
2182
  fill: "#8b93a7",
2183
+ // Dark halo so the label reads over the seat dots at any zoom.
2184
+ shadowColor: "#05070c",
2185
+ shadowBlur: 6,
2186
+ shadowOpacity: 0.9,
2187
+ shadowForStrokeEnabled: false,
1532
2188
  listening: false,
1533
2189
  perfectDrawEnabled: false
1534
2190
  });
@@ -1538,11 +2194,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1538
2194
  const subLabel = new import_Text.Text({
1539
2195
  x: centroid.x,
1540
2196
  y: centroid.y,
1541
- text: `${free} LEFT`,
2197
+ text: t("map.seatsLeft", { count: free }),
1542
2198
  fontSize: 12,
1543
- fontStyle: "600",
2199
+ fontStyle: "700",
1544
2200
  fontFamily: "JetBrains Mono, ui-monospace, monospace",
1545
- fill: rgba("#e6e9f0", 0.8),
2201
+ fill: "#f4f6fb",
2202
+ shadowColor: "#05070c",
2203
+ shadowBlur: 5,
2204
+ shadowOpacity: 0.9,
2205
+ shadowForStrokeEnabled: false,
1546
2206
  opacity: 0,
1547
2207
  listening: false,
1548
2208
  perfectDrawEnabled: false
@@ -1561,6 +2221,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1561
2221
  baseFill,
1562
2222
  outlinePoly,
1563
2223
  blockPoly,
2224
+ rowLines,
1564
2225
  nameLabel,
1565
2226
  subLabel,
1566
2227
  elevation,
@@ -1576,7 +2237,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1576
2237
  refreshSectionFill(sec) {
1577
2238
  const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
1578
2239
  sec.blockPoly.fill(darken(sec.baseFill, sold * SOLD_DARKEN));
1579
- sec.subLabel.text(`${sec.free} LEFT`);
2240
+ sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
1580
2241
  sec.subLabel.offsetX(sec.subLabel.width() / 2);
1581
2242
  }
1582
2243
  /**
@@ -1594,8 +2255,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1594
2255
  for (const z of doc.zones) {
1595
2256
  const members = byZone.get(z.id);
1596
2257
  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;
2258
+ let anchor = members[0];
2259
+ for (const s of members) if (s.centroid.y < anchor.centroid.y) anchor = s;
2260
+ const cx = anchor.centroid.x;
2261
+ const cy = anchor.centroid.y;
1599
2262
  let minPrice = Infinity;
1600
2263
  for (const sec of members) {
1601
2264
  for (const id of sec.memberIds) {
@@ -1613,6 +2276,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1613
2276
  letterSpacing: 2,
1614
2277
  fontFamily: this.labelFont(),
1615
2278
  fill: z.color ?? "#f2f4f8",
2279
+ shadowColor: "#05070c",
2280
+ shadowBlur: 10,
2281
+ shadowOpacity: 0.95,
2282
+ shadowForStrokeEnabled: false,
1616
2283
  opacity: 0,
1617
2284
  listening: false,
1618
2285
  perfectDrawEnabled: false
@@ -1625,7 +2292,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1625
2292
  sub = new import_Text.Text({
1626
2293
  x: cx,
1627
2294
  y: cy,
1628
- text: `FROM $${minPrice}`,
2295
+ text: t("map.fromPrice", { price: formatMoney(minPrice, this.currency) }),
1629
2296
  fontSize: 14,
1630
2297
  fontStyle: "600",
1631
2298
  fontFamily: "JetBrains Mono, ui-monospace, monospace",
@@ -1656,7 +2323,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1656
2323
  blockT = scale <= SECTION_PROMINENT_SCALE ? 1 : 0;
1657
2324
  zoneT = scale <= ZONE_PROMINENT_SCALE ? 1 : 0;
1658
2325
  } else {
1659
- blockT = clamp((CACHE_THRESHOLD - scale) / (CACHE_THRESHOLD - SECTION_PROMINENT_SCALE), 0, 1);
2326
+ blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
1660
2327
  zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
1661
2328
  }
1662
2329
  if (!this.zones.length) zoneT = 0;
@@ -1666,7 +2333,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1666
2333
  if (rescale) this.lodScale = scale;
1667
2334
  for (const sec of this.sections) {
1668
2335
  sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT);
1669
- sec.nameLabel.fill(lerpColor("#8b93a7", "#f2f4f8", blockT));
2336
+ for (const line of sec.rowLines) line.opacity(blockT * (1 - zoneT));
2337
+ sec.nameLabel.fill(lerpColor("#aab3c5", "#ffffff", blockT));
1670
2338
  sec.nameLabel.opacity(1 - zoneT);
1671
2339
  sec.subLabel.opacity(blockT * (1 - zoneT));
1672
2340
  if (rescale) {
@@ -1674,9 +2342,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1674
2342
  this.sizeLabel(sec.subLabel, SECTION_SUB_PX / sx, sec.centroid.y + SECTION_LABEL_PX / sx);
1675
2343
  }
1676
2344
  }
2345
+ const zoneOpacity = zoneT * (1 - this.isoT);
1677
2346
  for (const zone of this.zones) {
1678
- zone.label.opacity(zoneT);
1679
- if (zone.sub) zone.sub.opacity(zoneT);
2347
+ zone.label.opacity(zoneOpacity);
2348
+ if (zone.sub) zone.sub.opacity(zoneOpacity);
1680
2349
  if (rescale) {
1681
2350
  const cy = zone.label.y();
1682
2351
  this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, cy);
@@ -1686,11 +2355,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1686
2355
  this.bgLayer.batchDraw();
1687
2356
  }
1688
2357
  /** 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);
2358
+ sizeLabel(t2, fontSize, y) {
2359
+ t2.fontSize(Math.max(1, fontSize));
2360
+ t2.offsetX(t2.width() / 2);
2361
+ t2.offsetY(t2.height() / 2);
2362
+ t2.y(y);
1694
2363
  }
1695
2364
  /**
1696
2365
  * Map a container-relative screen point back to world coords. Inverts the
@@ -1714,7 +2383,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1714
2383
  return this.sections.find((s) => s.id === id)?.memberIds.slice() ?? [];
1715
2384
  }
1716
2385
  addCentredLabel(layer, text, x, y, fill, fontSize, bold) {
1717
- const t = new import_Text.Text({
2386
+ const t2 = new import_Text.Text({
1718
2387
  x,
1719
2388
  y,
1720
2389
  text,
@@ -1725,9 +2394,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1725
2394
  listening: false,
1726
2395
  perfectDrawEnabled: false
1727
2396
  });
1728
- t.offsetX(t.width() / 2);
1729
- t.offsetY(t.height() / 2);
1730
- layer.add(t);
2397
+ t2.offsetX(t2.width() / 2);
2398
+ t2.offsetY(t2.height() / 2);
2399
+ layer.add(t2);
1731
2400
  }
1732
2401
  // ---- selection ------------------------------------------------------------
1733
2402
  isSelectable(id) {
@@ -1781,7 +2450,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1781
2450
  this.seatLayer.on("click tap", (e) => {
1782
2451
  if (this.moved > 8) return;
1783
2452
  const id = seatIdOf(e.target);
1784
- if (id) this.toggleSeat(id);
2453
+ if (!id) return;
2454
+ if (this.stacked && this.opts.onDeckTap) {
2455
+ const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
2456
+ if (floorId) {
2457
+ this.opts.onDeckTap(floorId);
2458
+ return;
2459
+ }
2460
+ }
2461
+ this.toggleSeat(id);
1785
2462
  });
1786
2463
  this.seatLayer.on("mouseover", (e) => {
1787
2464
  const id = seatIdOf(e.target);
@@ -1811,11 +2488,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1811
2488
  if (!this.cached || this.moved > 8) return;
1812
2489
  const pointer = this.stage.getPointerPosition();
1813
2490
  if (!pointer) return;
2491
+ if (this.stacked && this.opts.onDeckTap) {
2492
+ const floorId = this.deckFloorAt();
2493
+ if (floorId) {
2494
+ this.opts.onDeckTap(floorId);
2495
+ return;
2496
+ }
2497
+ }
1814
2498
  if (this.sections.length) {
1815
2499
  const world = this.screenToWorld(pointer);
1816
2500
  const hit = this.sections.find((sn) => pointInPolygon(world, sn.outline));
1817
2501
  if (hit) {
1818
- this.zoomToBounds(polyBounds(hit.outline));
2502
+ if (this.opts.onSectionTap) this.opts.onSectionTap(hit.id);
2503
+ else this.focusRegion(hit.id);
1819
2504
  return;
1820
2505
  }
1821
2506
  }
@@ -1823,6 +2508,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1823
2508
  this.zoomAbout(target, pointer);
1824
2509
  });
1825
2510
  }
2511
+ /**
2512
+ * Which deck (floor) a screen tap landed on in the 3D stacked overview. The
2513
+ * seat layer is melt-cached at that zoom so individual seat nodes aren't
2514
+ * hit-testable; instead we invert the iso+stage transform via the layer's
2515
+ * relative pointer and take the nearest seat's floor. Only the floor matters,
2516
+ * so within-floor elevation offsets don't affect the result. Null if none.
2517
+ */
2518
+ deckFloorAt() {
2519
+ if (!this.objectFloor.size) return null;
2520
+ const p = this.seatLayer.getRelativePointerPosition();
2521
+ if (!p) return null;
2522
+ let best = null;
2523
+ let bestD = Infinity;
2524
+ for (const s of this.seats) {
2525
+ const dx = s.x - p.x;
2526
+ const dy = s.y - p.y;
2527
+ const d = dx * dx + dy * dy;
2528
+ if (d < bestD) {
2529
+ bestD = d;
2530
+ best = s.rowId;
2531
+ }
2532
+ }
2533
+ return best ? this.objectFloor.get(best) ?? null : null;
2534
+ }
1826
2535
  // ---- Pan & pinch (raw pointer events — mouse, touch and pen alike) --------
1827
2536
  toLocal(e) {
1828
2537
  const r = this.container.getBoundingClientRect();
@@ -1833,7 +2542,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1833
2542
  * reach a readable size on any chart, however large the venue.
1834
2543
  */
1835
2544
  zoomBounds() {
1836
- return { min: 0.5 * this.fitScale, max: Math.max(10 * this.fitScale, 4) };
2545
+ let min = 0.5 * this.fitScale;
2546
+ if (this.zones.length) min = Math.min(min, ZONE_PROMINENT_SCALE * 0.9);
2547
+ return { min, max: Math.max(10 * this.fitScale, 4) };
1837
2548
  }
1838
2549
  /** Zoom so `clientPoint` (stage-relative px) stays fixed under the cursor. */
1839
2550
  zoomAbout(nextScale, clientPoint) {
@@ -1866,6 +2577,92 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1866
2577
  this.afterViewChange();
1867
2578
  this.stage.batchDraw();
1868
2579
  }
2580
+ /**
2581
+ * Smoothly glide the camera to frame a section (by id) or a world-space bounds
2582
+ * rect — the Slice 5 "glide in". Pan+zoom tween over ~450ms easeInOutCubic; the
2583
+ * melt/LOD rides the camera every frame. `prefers-reduced-motion` (or
2584
+ * `opts.animate === false`) snaps via zoomToBounds. A grab (pointer-down) or a
2585
+ * newer glide cancels an in-flight one.
2586
+ */
2587
+ focusRegion(target, opts) {
2588
+ const b = typeof target === "string" ? (() => {
2589
+ const sec = this.sections.find((s) => s.id === target);
2590
+ return sec ? polyBounds(sec.outline) : null;
2591
+ })() : target;
2592
+ if (!b) return;
2593
+ this.cancelGlide();
2594
+ if (opts?.animate === false || this.reducedMotion) {
2595
+ this.zoomToBounds(b);
2596
+ return;
2597
+ }
2598
+ const w = this.stage.width();
2599
+ const h = this.stage.height();
2600
+ const { min, max } = this.zoomBounds();
2601
+ const margin = 1.12;
2602
+ const toScale = clamp(Math.min(w / (b.width * margin), h / (b.height * margin)), min, max);
2603
+ const toX = w / 2 - (b.x + b.width / 2) * toScale;
2604
+ const toY = h / 2 - (b.y + b.height / 2) * toScale;
2605
+ const fromScale = this.stage.scaleX();
2606
+ const fromX = this.stage.x();
2607
+ const fromY = this.stage.y();
2608
+ if (Math.abs(toScale - fromScale) < 1e-4 && Math.abs(toX - fromX) < 0.5 && Math.abs(toY - fromY) < 0.5) {
2609
+ this.afterViewChange();
2610
+ return;
2611
+ }
2612
+ const start = performance.now();
2613
+ const DUR = 450;
2614
+ const step = (now) => {
2615
+ if (this.destroyed) {
2616
+ this.glideRaf = 0;
2617
+ return;
2618
+ }
2619
+ const raw = Math.min(1, (now - start) / DUR);
2620
+ const e = raw < 0.5 ? 4 * raw * raw * raw : 1 - Math.pow(-2 * raw + 2, 3) / 2;
2621
+ const sc = fromScale + (toScale - fromScale) * e;
2622
+ this.stage.scale({ x: sc, y: sc });
2623
+ this.stage.position({ x: fromX + (toX - fromX) * e, y: fromY + (toY - fromY) * e });
2624
+ this.updateLOD();
2625
+ this.scheduleViewChange();
2626
+ this.stage.batchDraw();
2627
+ if (raw < 1) {
2628
+ this.glideRaf = requestAnimationFrame(step);
2629
+ } else {
2630
+ this.glideRaf = 0;
2631
+ this.afterViewChange();
2632
+ }
2633
+ };
2634
+ this.glideRaf = requestAnimationFrame(step);
2635
+ }
2636
+ /** Cancel an in-flight camera glide (a grab or a newer glide interrupts it). */
2637
+ cancelGlide() {
2638
+ if (this.glideRaf) {
2639
+ cancelAnimationFrame(this.glideRaf);
2640
+ this.glideRaf = 0;
2641
+ }
2642
+ }
2643
+ /** Current LOD rung derived from effective zoom — drives the ZONES/SECTIONS/SEATS pill. */
2644
+ getRung() {
2645
+ const scale = this.effScale();
2646
+ if (scale >= CACHE_THRESHOLD) return "seats";
2647
+ if (this.zones.length && scale < ZONE_PROMINENT_SCALE) return "zones";
2648
+ return "sections";
2649
+ }
2650
+ /** Jump the camera to a rung's zoom band, centred on the chart (glided). */
2651
+ setRung(rung) {
2652
+ if (rung === "zones") {
2653
+ this.cancelGlide();
2654
+ this.zoomToFit();
2655
+ return;
2656
+ }
2657
+ const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(SEAT_LEGIBLE_SCALE * 1.1, CACHE_THRESHOLD * 1.3);
2658
+ const w = this.stage.width();
2659
+ const h = this.stage.height();
2660
+ const cx = this.bounds.x + this.bounds.width / 2;
2661
+ const cy = this.bounds.y + this.bounds.height / 2;
2662
+ const bw = w / (target * 1.12);
2663
+ const bh = h / (target * 1.12);
2664
+ this.focusRegion({ x: cx - bw / 2, y: cy - bh / 2, width: bw, height: bh });
2665
+ }
1869
2666
  /** Recompute LOD (cache/labels) after any pan/zoom settles. */
1870
2667
  afterViewChange() {
1871
2668
  this.updateLOD();
@@ -1916,7 +2713,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1916
2713
  let count = 0;
1917
2714
  for (const seat of this.seats) {
1918
2715
  if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
1919
- const t = new import_Text.Text({
2716
+ const t2 = new import_Text.Text({
1920
2717
  x: seat.x,
1921
2718
  y: seat.y,
1922
2719
  text: seat.label,
@@ -1928,14 +2725,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1928
2725
  perfectDrawEnabled: false
1929
2726
  });
1930
2727
  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();
2728
+ if (t2.width() > maxW) t2.fontSize(Math.max(4, 7 * maxW / t2.width()));
2729
+ if (t2.fontSize() < 4.2) {
2730
+ t2.destroy();
1934
2731
  continue;
1935
2732
  }
1936
- t.offsetX(t.width() / 2);
1937
- t.offsetY(t.height() / 2);
1938
- this.labelGroup.add(t);
2733
+ t2.offsetX(t2.width() / 2);
2734
+ t2.offsetY(t2.height() / 2);
2735
+ this.labelGroup.add(t2);
1939
2736
  if (++count >= MAX_LABELS) break;
1940
2737
  }
1941
2738
  if (this.isoT > 0) this.applyUprightLabels();
@@ -1986,9 +2783,15 @@ var PickerController = class {
1986
2783
  constructor(options) {
1987
2784
  this.renderer = null;
1988
2785
  this._doc = null;
2786
+ /** Section/zone ids hidden from buyers this event (3.3) — seats vanish, not grey. */
2787
+ this.hidden = /* @__PURE__ */ new Set();
1989
2788
  /** label ⇄ id maps — backend speaks labels, the engine speaks ids. */
1990
2789
  this.labelToId = /* @__PURE__ */ new Map();
1991
2790
  this.labelToSeat = /* @__PURE__ */ new Map();
2791
+ /** seatId → chosen ticket-tier id (absent ⇒ the category's first/default tier). */
2792
+ this.seatTiers = /* @__PURE__ */ new Map();
2793
+ /** id → seat, for the section-summary breakdown (renderer members are ids). */
2794
+ this.seatById = /* @__PURE__ */ new Map();
1992
2795
  this.allIds = [];
1993
2796
  // realtime socket
1994
2797
  this.ws = null;
@@ -2006,6 +2809,18 @@ var PickerController = class {
2006
2809
  get doc() {
2007
2810
  return this._doc;
2008
2811
  }
2812
+ /** The chart with hidden sections' seats removed — what buyers actually see. */
2813
+ visibleDoc() {
2814
+ return this._doc ? applyHidden(this._doc, this.hidden) : { objects: [] };
2815
+ }
2816
+ /** Adopt a new hidden set; rebuild the visible chart only if it differs. */
2817
+ syncHidden(ids) {
2818
+ const next = ids.filter((x) => typeof x === "string");
2819
+ if (next.length === this.hidden.size && next.every((id) => this.hidden.has(id))) return false;
2820
+ this.hidden = new Set(next);
2821
+ this.renderer?.setChart(this.visibleDoc());
2822
+ return true;
2823
+ }
2009
2824
  currentHold() {
2010
2825
  return this.hold_;
2011
2826
  }
@@ -2033,15 +2848,19 @@ var PickerController = class {
2033
2848
  this._doc = res.doc;
2034
2849
  this.labelToId = /* @__PURE__ */ new Map();
2035
2850
  this.labelToSeat = /* @__PURE__ */ new Map();
2851
+ this.seatById = /* @__PURE__ */ new Map();
2036
2852
  this.allIds = [];
2037
2853
  for (const s of expandChart(res.doc)) {
2038
2854
  this.labelToId.set(s.label, s.id);
2039
2855
  this.labelToSeat.set(s.label, s);
2856
+ this.seatById.set(s.id, s);
2040
2857
  this.allIds.push(s.id);
2041
2858
  }
2859
+ const currency = res.event.currency ?? this.opts.currency;
2042
2860
  const renderer = createRenderer(host, {
2043
2861
  maxSelection: this.maxSelection,
2044
2862
  confirmSelection: this.opts.confirmSelection,
2863
+ currency,
2045
2864
  onSelect: (seat) => {
2046
2865
  this.opts.onSelect?.(seat);
2047
2866
  this.emitSelectionChange();
@@ -2054,6 +2873,9 @@ var PickerController = class {
2054
2873
  onFocusSeat: this.opts.onFocusSeat,
2055
2874
  onViewChange: this.opts.onViewChange,
2056
2875
  onGAClick: this.opts.onGAClick,
2876
+ // Section tap at the far/zone rung → glide in + surface the summary card.
2877
+ onSectionTap: (id) => this.handleSectionTap(id),
2878
+ onDeckTap: (floorId) => this.handleDeckTap(floorId),
2057
2879
  onFps: this.opts.onFps
2058
2880
  });
2059
2881
  if (this.closed) {
@@ -2061,15 +2883,28 @@ var PickerController = class {
2061
2883
  return null;
2062
2884
  }
2063
2885
  this.renderer = renderer;
2064
- renderer.setChart(res.doc);
2065
- await this.resnapshot();
2886
+ let seats = null;
2887
+ try {
2888
+ const objs = await this.api.objects(this.key);
2889
+ this.hidden = new Set(objs.hidden ?? []);
2890
+ seats = objs.seats;
2891
+ } catch {
2892
+ }
2893
+ if (this.closed) {
2894
+ renderer.destroy();
2895
+ this.renderer = null;
2896
+ return null;
2897
+ }
2898
+ renderer.setChart(this.visibleDoc());
2899
+ if (seats) this.applySeatsMap(seats);
2066
2900
  this.connect();
2067
2901
  return {
2068
2902
  doc: res.doc,
2069
2903
  salesClosed: !!res.event.salesClosed,
2070
2904
  eventName: res.event.name,
2071
2905
  venue: res.event.venue,
2072
- startsAt: res.event.startsAt
2906
+ startsAt: res.event.startsAt,
2907
+ currency
2073
2908
  };
2074
2909
  }
2075
2910
  // ---- selection ------------------------------------------------------------
@@ -2241,6 +3076,126 @@ var PickerController = class {
2241
3076
  setAccessibilityFilter(types) {
2242
3077
  this.renderer?.setAccessibilityFilter?.(types);
2243
3078
  }
3079
+ // ---- multi-floor (Batch 5) ------------------------------------------------
3080
+ /** Floors for the buyer's switcher (>1 ⇒ show it). Single-floor ⇒ one entry. */
3081
+ getFloors() {
3082
+ return this.renderer?.getFloors?.() ?? [];
3083
+ }
3084
+ getActiveFloorId() {
3085
+ return this.renderer?.getActiveFloorId?.() ?? "";
3086
+ }
3087
+ /** Switch the shown floor (2D), then re-apply live seat statuses onto it. */
3088
+ setFloor(id) {
3089
+ const r = this.renderer;
3090
+ if (!r?.setActiveFloor || id === r.getActiveFloorId?.()) return;
3091
+ r.setStacked?.(false);
3092
+ r.setActiveFloor(id);
3093
+ void this.resnapshot();
3094
+ this.emitSelectionChange();
3095
+ }
3096
+ /** 3D all-floors stacked overview ⇄ active floor. Re-applies statuses after. */
3097
+ setStacked(on) {
3098
+ const r = this.renderer;
3099
+ if (!r?.setStacked || on === r.isStacked?.()) return;
3100
+ r.setStacked(on);
3101
+ void this.resnapshot();
3102
+ }
3103
+ isMultiFloor() {
3104
+ return (this._doc?.floors?.length ?? 0) > 1;
3105
+ }
3106
+ /** Tap-a-deck-to-enter: leave the 3D stack, drop to flat 2D on `floorId`, and
3107
+ * tell the host so it can sync its 2D/3D toggle + floor-switcher state. */
3108
+ handleDeckTap(floorId) {
3109
+ const r = this.renderer;
3110
+ if (!r) return;
3111
+ r.setViewMode?.("flat");
3112
+ r.setStacked?.(false);
3113
+ r.setActiveFloor?.(floorId);
3114
+ void this.resnapshot();
3115
+ this.emitSelectionChange();
3116
+ this.opts.onDeckTap?.(floorId);
3117
+ }
3118
+ // ---- big-venue: sections / rungs / projection (Slice 5) -------------------
3119
+ /** Switch the map projection (2D flat ⇄ 3D isometric). No-op on a flat renderer. */
3120
+ setViewMode(mode) {
3121
+ this.renderer?.setViewMode?.(mode);
3122
+ }
3123
+ getViewMode() {
3124
+ return this.renderer?.getViewMode?.() ?? "flat";
3125
+ }
3126
+ /** Current LOD rung (for the ZONES/SECTIONS/SEATS pill). */
3127
+ getRung() {
3128
+ return this.renderer?.getRung?.() ?? "seats";
3129
+ }
3130
+ /** Jump to a rung; ZONES clears any focused summary (back to overview). */
3131
+ setRung(rung) {
3132
+ if (rung === "zones") {
3133
+ this.overview();
3134
+ return;
3135
+ }
3136
+ this.renderer?.setRung?.(rung);
3137
+ }
3138
+ /** Glide in on a section and surface its summary (same path as a section tap). */
3139
+ focusSection(id) {
3140
+ this.handleSectionTap(id);
3141
+ }
3142
+ /** Zoom back out to the whole chart and clear the section-summary card. */
3143
+ overview() {
3144
+ this.renderer?.zoomToFit();
3145
+ this.opts.onSectionFocus?.(null);
3146
+ }
3147
+ /** Glide the camera into a tapped section and emit its computed summary. */
3148
+ handleSectionTap(id) {
3149
+ const r = this.renderer;
3150
+ if (!r) return;
3151
+ r.focusRegion?.(id);
3152
+ this.opts.onSectionFocus?.(this.sectionSummary(id));
3153
+ }
3154
+ /**
3155
+ * Build a section summary from the renderer's spatial membership: section +
3156
+ * zone labels, live seats-left, price range, and the per-category breakdown.
3157
+ */
3158
+ sectionSummary(id) {
3159
+ const r = this.renderer;
3160
+ const doc = this._doc;
3161
+ if (!r || !doc) return null;
3162
+ const sec = doc.objects.find(
3163
+ (o) => o.type === "section" && o.id === id
3164
+ );
3165
+ if (!sec) return null;
3166
+ const memberIds = r.sectionMembers?.(id) ?? [];
3167
+ const byCat = /* @__PURE__ */ new Map();
3168
+ let seatsLeft = 0;
3169
+ for (const sid of memberIds) {
3170
+ const seat = this.seatById.get(sid);
3171
+ if (!seat) continue;
3172
+ byCat.set(seat.categoryKey, (byCat.get(seat.categoryKey) ?? 0) + 1);
3173
+ if (r.getStatus(sid) === "free") seatsLeft++;
3174
+ }
3175
+ const categories = [...byCat.entries()].map(([key, count]) => {
3176
+ const cat = doc.categories.find((c) => c.key === key);
3177
+ return {
3178
+ key,
3179
+ count,
3180
+ label: cat?.label ?? key,
3181
+ color: cat?.color ?? "#6e7bff",
3182
+ price: cat?.price ?? 0
3183
+ };
3184
+ }).sort((a, b) => a.price - b.price);
3185
+ const prices = categories.map((c) => c.price);
3186
+ const zone = sec.zone ? doc.zones?.find((z) => z.id === sec.zone) : void 0;
3187
+ const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
3188
+ return {
3189
+ id,
3190
+ label: sec.label,
3191
+ zoneLabel: zone?.label ?? "",
3192
+ color,
3193
+ seatsLeft,
3194
+ priceMin: prices.length ? prices[0] : 0,
3195
+ priceMax: prices.length ? prices[prices.length - 1] : 0,
3196
+ categories
3197
+ };
3198
+ }
2244
3199
  destroy() {
2245
3200
  this.closed = true;
2246
3201
  if (this.reconnectTimer) {
@@ -2263,11 +3218,53 @@ var PickerController = class {
2263
3218
  }
2264
3219
  // ---- internals ------------------------------------------------------------
2265
3220
  toSeat(s) {
2266
- return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey) };
3221
+ const tiers = this.tiersFor(s.categoryKey);
3222
+ if (!tiers) {
3223
+ return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey) };
3224
+ }
3225
+ const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
3226
+ return {
3227
+ id: s.id,
3228
+ label: s.label,
3229
+ categoryKey: s.categoryKey,
3230
+ price: chosen.price,
3231
+ tiers,
3232
+ tierId: chosen.id
3233
+ };
2267
3234
  }
2268
3235
  priceFor(categoryKey) {
2269
3236
  return this._doc?.categories.find((c) => c.key === categoryKey)?.price ?? 0;
2270
3237
  }
3238
+ tiersFor(categoryKey) {
3239
+ const t2 = this._doc?.categories.find((c) => c.key === categoryKey)?.tiers;
3240
+ return t2 && t2.length ? t2 : void 0;
3241
+ }
3242
+ /**
3243
+ * Choose a ticket tier for a selected seat (e.g. Adult → Child). Re-emits the
3244
+ * selection so the host's cart + the eventual hold carry the new tier + price.
3245
+ * `tierId = null` reverts to the category's first (default) tier. No-op if the
3246
+ * seat's category has no tiers or the id isn't one of them.
3247
+ */
3248
+ setSeatTier(seatId, tierId) {
3249
+ const seat = this.seatById.get(seatId);
3250
+ if (!seat) return;
3251
+ const tiers = this.tiersFor(seat.categoryKey);
3252
+ if (!tiers) return;
3253
+ if (tierId == null) this.seatTiers.delete(seatId);
3254
+ else if (tiers.some((t2) => t2.id === tierId)) this.seatTiers.set(seatId, tierId);
3255
+ else return;
3256
+ this.emitSelectionChange();
3257
+ if (this.hold_) this.hold_ = { ...this.hold_, seats: this.seatsForLabels(this.hold_.labels) };
3258
+ }
3259
+ /** PickerSeats (with chosen tier) for a set of held labels. */
3260
+ seatsForLabels(labels) {
3261
+ const out = [];
3262
+ for (const l of labels) {
3263
+ const s = this.labelToSeat.get(l);
3264
+ if (s) out.push(this.toSeat(s));
3265
+ }
3266
+ return out;
3267
+ }
2271
3268
  emitSelectionChange() {
2272
3269
  this.opts.onSelectionChange?.(this.getSelection());
2273
3270
  }
@@ -2293,11 +3290,12 @@ var PickerController = class {
2293
3290
  }
2294
3291
  /** Set the open hold + (re)arm the server-authoritative expiry timer. */
2295
3292
  setHold(hold) {
2296
- this.hold_ = hold;
3293
+ const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
3294
+ this.hold_ = full;
2297
3295
  if (this.expiryTimer) clearTimeout(this.expiryTimer);
2298
- const ms = Math.max(0, hold.expiresAt - Date.now());
3296
+ const ms = Math.max(0, full.expiresAt - Date.now());
2299
3297
  this.expiryTimer = setTimeout(() => this.onHoldExpired(), ms);
2300
- this.opts.onHold?.(hold);
3298
+ this.opts.onHold?.(full);
2301
3299
  }
2302
3300
  clearHold() {
2303
3301
  this.hold_ = null;
@@ -2361,6 +3359,11 @@ var PickerController = class {
2361
3359
  const r = this.renderer;
2362
3360
  if (!r || !msg || typeof msg !== "object") return;
2363
3361
  const m = msg;
3362
+ if (Array.isArray(m.hidden) && this.syncHidden(m.hidden)) {
3363
+ void this.resnapshot();
3364
+ this.opts.onStatusChange?.();
3365
+ }
3366
+ if (m.type === "hidden") return;
2364
3367
  if (m.seats && typeof m.seats === "object") {
2365
3368
  this.applySeatsMap(m.seats);
2366
3369
  } else if (Array.isArray(m.changes)) {
@@ -2397,22 +3400,68 @@ var PickerController = class {
2397
3400
  }, delay);
2398
3401
  }
2399
3402
  };
3403
+
3404
+ // src/i18n/bundles.ts
3405
+ var LOADERS = {
3406
+ es: () => Promise.resolve().then(() => (init_es(), es_exports)).then((m) => ({ default: m.es })),
3407
+ de: () => Promise.resolve().then(() => (init_de(), de_exports)).then((m) => ({ default: m.de })),
3408
+ fr: () => Promise.resolve().then(() => (init_fr(), fr_exports)).then((m) => ({ default: m.fr }))
3409
+ };
3410
+ var loaded = /* @__PURE__ */ new Set(["en"]);
3411
+ async function loadLocale(code) {
3412
+ const locale = resolveLocale(code);
3413
+ if (locale === "en" || loaded.has(locale)) {
3414
+ setLocale(locale);
3415
+ return locale;
3416
+ }
3417
+ try {
3418
+ const mod = await LOADERS[locale]();
3419
+ setLocale(locale, mod.default);
3420
+ loaded.add(locale);
3421
+ return locale;
3422
+ } catch {
3423
+ setLocale("en");
3424
+ return "en";
3425
+ }
3426
+ }
2400
3427
  // Annotate the CommonJS export names for ESM import in node:
2401
3428
  0 && (module.exports = {
2402
3429
  ACCESSIBILITY_TYPES,
2403
3430
  CHART_STORAGE_KEY,
3431
+ DEFAULT_CURRENCY,
2404
3432
  PickerController,
3433
+ SUPPORTED_LOCALES,
2405
3434
  SeatmapRenderer,
3435
+ UNGROUPED_ID,
2406
3436
  accessibilityMeta,
3437
+ allObjects,
3438
+ applyHidden,
2407
3439
  chartBounds,
3440
+ computeSections,
2408
3441
  createRenderer,
3442
+ currencySymbol,
2409
3443
  expandBooth,
2410
3444
  expandChart,
2411
3445
  expandRow,
2412
3446
  expandRowSlots,
2413
3447
  expandTable,
3448
+ floorObjects,
3449
+ floorsOf,
3450
+ formatDate,
3451
+ formatMoney,
3452
+ getLocale,
3453
+ hiddenObjectIds,
3454
+ isSectionHidden,
2414
3455
  layerOf,
3456
+ loadLocale,
2415
3457
  objectCenter,
2416
- pointInPolygon
3458
+ pointInPolygon,
3459
+ resolveLocale,
3460
+ setLocale,
3461
+ setMoneyLocale,
3462
+ setStringOverrides,
3463
+ stackFloors,
3464
+ t,
3465
+ tCount
2417
3466
  });
2418
3467
  //# sourceMappingURL=index.cjs.map