@seatlayer/core 0.1.1 → 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.js CHANGED
@@ -78,7 +78,15 @@ function expandRowSlots(row) {
78
78
  const start = row.seatLabelStart ?? 1;
79
79
  const dir = row.seatNumbering?.direction ?? "ltr";
80
80
  const step = row.seatNumbering?.step ?? 1;
81
- const seatNumber = (i) => start + (dir === "rtl" ? row.seatCount - 1 - i : i) * step;
81
+ const n = row.seatCount;
82
+ let seatNumber;
83
+ if (dir === "center") {
84
+ const rank = new Array(n);
85
+ 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);
86
+ seatNumber = (i) => start + rank[i] * step;
87
+ } else {
88
+ seatNumber = (i) => start + (dir === "rtl" ? n - 1 - i : i) * step;
89
+ }
82
90
  const ov = overrideMap(row);
83
91
  return rowSeatPositions(row).map((p, i) => {
84
92
  const o = ov.get(i);
@@ -113,30 +121,41 @@ function expandRow(row) {
113
121
  }
114
122
  return seats;
115
123
  }
116
- function expandTable(t) {
124
+ function expandTable(t2) {
117
125
  const seats = [];
118
- const n = Math.max(0, Math.round(t.seatCount));
126
+ const n = Math.max(0, Math.round(t2.seatCount));
119
127
  if (n === 0) return seats;
120
128
  const mk = (i, x, y) => ({
121
- id: `${t.id}:${i}`,
122
- label: `${t.label}-${i + 1}`,
129
+ id: `${t2.id}:${i}`,
130
+ label: `${t2.label}-${i + 1}`,
123
131
  x,
124
132
  y,
125
- rowId: t.id,
126
- categoryKey: t.categoryKey
133
+ rowId: t2.id,
134
+ categoryKey: t2.categoryKey
127
135
  });
128
- if (t.shape === "round") {
129
- const R = (t.radius ?? 40) + TABLE_SEAT_OFFSET;
130
- const base = t.rotation * DEG;
136
+ if (t2.shape === "round") {
137
+ const R = (t2.radius ?? 40) + TABLE_SEAT_OFFSET;
138
+ const base = t2.rotation * DEG;
139
+ const arc = Math.max(0, Math.min(360, t2.seatArc ?? 360));
140
+ if (arc >= 360 || n === 1) {
141
+ for (let i = 0; i < n; i++) {
142
+ const a = base + i / n * 2 * Math.PI;
143
+ seats.push(mk(i, t2.center.x + R * Math.cos(a), t2.center.y + R * Math.sin(a)));
144
+ }
145
+ return seats;
146
+ }
147
+ const arcRad = arc * DEG;
148
+ const halfGap = (2 * Math.PI - arcRad) / 2;
149
+ const start = base + halfGap;
131
150
  for (let i = 0; i < n; i++) {
132
- const a = base + i / n * 2 * Math.PI;
133
- seats.push(mk(i, t.center.x + R * Math.cos(a), t.center.y + R * Math.sin(a)));
151
+ const a = start + i / (n - 1) * arcRad;
152
+ seats.push(mk(i, t2.center.x + R * Math.cos(a), t2.center.y + R * Math.sin(a)));
134
153
  }
135
154
  return seats;
136
155
  }
137
- const w = t.width ?? 80;
138
- const h = t.height ?? 50;
139
- const enabled = t.sides && t.sides.length ? t.sides : ["top", "bottom"];
156
+ const w = t2.width ?? 80;
157
+ const h = t2.height ?? 50;
158
+ const enabled = t2.sides && t2.sides.length ? t2.sides : ["top", "bottom"];
140
159
  const order = ["top", "bottom", "left", "right"].filter((s) => enabled.includes(s));
141
160
  if (!order.length) return seats;
142
161
  const counts = new Map(order.map((s) => [s, 0]));
@@ -163,7 +182,7 @@ function expandTable(t) {
163
182
  localX = w / 2 + TABLE_SEAT_OFFSET;
164
183
  localY = -h / 2 + (j + 0.5) * h / count;
165
184
  }
166
- const p = place(localX, localY, t.rotation, t.center);
185
+ const p = place(localX, localY, t2.rotation, t2.center);
167
186
  seats.push(mk(idx++, p.x, p.y));
168
187
  }
169
188
  }
@@ -234,9 +253,53 @@ function objectCenter(o) {
234
253
  return { x: o.x ?? 0, y: o.y ?? 0 };
235
254
  }
236
255
  }
256
+ function floorsOf(doc) {
257
+ if (doc.floors && doc.floors.length) return doc.floors;
258
+ return [{ id: "floor-0", name: "Main", objects: doc.objects, focalPoint: doc.focalPoint, backgroundImage: doc.backgroundImage }];
259
+ }
260
+ function floorObjects(doc, floorId) {
261
+ const floors = floorsOf(doc);
262
+ return (floorId ? floors.find((f) => f.id === floorId) : floors[0])?.objects ?? [];
263
+ }
264
+ function allObjects(doc) {
265
+ return doc.floors && doc.floors.length ? doc.floors.flatMap((f) => f.objects) : doc.objects;
266
+ }
267
+ function translateObject(o, dx, dy) {
268
+ const p = (pt) => ({ x: pt.x + dx, y: pt.y + dy });
269
+ const pts = (a) => a.map(p);
270
+ switch (o.type) {
271
+ case "row":
272
+ return { ...o, origin: p(o.origin) };
273
+ case "table":
274
+ case "booth":
275
+ return { ...o, center: p(o.center) };
276
+ case "gaArea":
277
+ return { ...o, points: pts(o.points) };
278
+ case "section":
279
+ return { ...o, outline: pts(o.outline) };
280
+ case "text":
281
+ return { ...o, position: p(o.position) };
282
+ case "shape":
283
+ return {
284
+ ...o,
285
+ ...o.points ? { points: pts(o.points) } : {},
286
+ ...o.x != null ? { x: o.x + dx } : {},
287
+ ...o.y != null ? { y: o.y + dy } : {}
288
+ };
289
+ }
290
+ }
291
+ function stackFloors(doc, spread = 900) {
292
+ if (!doc.floors || doc.floors.length < 2) return doc;
293
+ const objects = [];
294
+ doc.floors.forEach((f, i) => {
295
+ const dy = -i * spread;
296
+ for (const o of f.objects) objects.push(dy === 0 ? o : translateObject(o, 0, dy));
297
+ });
298
+ return { ...doc, objects, floors: void 0 };
299
+ }
237
300
  function expandChart(doc) {
238
301
  const out = [];
239
- for (const obj of doc.objects) {
302
+ for (const obj of allObjects(doc)) {
240
303
  if (obj.type === "row") out.push(...expandRow(obj));
241
304
  else if (obj.type === "table") out.push(...expandTable(obj));
242
305
  else if (obj.type === "booth") out.push(...expandBooth(obj));
@@ -256,7 +319,7 @@ function chartBounds(doc) {
256
319
  if (y > maxY) maxY = y;
257
320
  };
258
321
  for (const s of expandChart(doc)) acc(s.x, s.y);
259
- for (const obj of doc.objects) {
322
+ for (const obj of allObjects(doc)) {
260
323
  if (obj.type === "gaArea") {
261
324
  for (const p of obj.points) acc(p.x, p.y);
262
325
  } else if (obj.type === "section") {
@@ -301,6 +364,87 @@ function chartBounds(doc) {
301
364
  };
302
365
  }
303
366
 
367
+ // src/core/sections.ts
368
+ var UNGROUPED_ID = "__ungrouped__";
369
+ function objectSeatLabels(o) {
370
+ if (o.type === "row") return expandRow(o).map((s) => s.label);
371
+ if (o.type === "table") return expandTable(o).map((s) => s.label);
372
+ if (o.type === "booth") return expandBooth(o).map((s) => s.label);
373
+ return [];
374
+ }
375
+ function isSeatObject(o) {
376
+ return o.type === "row" || o.type === "table" || o.type === "booth";
377
+ }
378
+ function computeSections(doc) {
379
+ const objs = allObjects(doc);
380
+ const sectionObjs = objs.filter((o) => o.type === "section");
381
+ const nodes = /* @__PURE__ */ new Map();
382
+ for (const s of sectionObjs) {
383
+ nodes.set(s.id, { id: s.id, label: s.label || "Section", zone: s.zone, seatCount: 0, objectIds: [], seatLabels: [] });
384
+ }
385
+ const ungrouped = { id: UNGROUPED_ID, label: "Other seats", seatCount: 0, objectIds: [], seatLabels: [] };
386
+ const objectToSection = /* @__PURE__ */ new Map();
387
+ for (const obj of objs) {
388
+ if (!isSeatObject(obj)) continue;
389
+ const labels = objectSeatLabels(obj);
390
+ if (labels.length === 0) continue;
391
+ const c = objectCenter(obj);
392
+ const owner = sectionObjs.find((s) => pointInPolygon(c, s.outline));
393
+ const node = owner ? nodes.get(owner.id) : ungrouped;
394
+ node.seatCount += labels.length;
395
+ node.objectIds.push(obj.id);
396
+ node.seatLabels.push(...labels);
397
+ objectToSection.set(obj.id, node.id);
398
+ }
399
+ return {
400
+ sections: sectionObjs.map((s) => nodes.get(s.id)),
401
+ ungrouped: ungrouped.objectIds.length ? ungrouped : null,
402
+ objectToSection
403
+ };
404
+ }
405
+ function isSectionHidden(s, hidden) {
406
+ return hidden.has(s.id) || !!s.zone && hidden.has(s.zone);
407
+ }
408
+ function hiddenObjectIds(doc, hidden) {
409
+ const out = /* @__PURE__ */ new Set();
410
+ if (!hidden.size) return out;
411
+ const { sections, ungrouped, objectToSection } = computeSections(doc);
412
+ const hiddenSectionIds = /* @__PURE__ */ new Set();
413
+ const zoneById = new Map(sections.map((s) => [s.id, s.zone]));
414
+ for (const s of sections) {
415
+ const zone = zoneById.get(s.id);
416
+ if (hidden.has(s.id) || zone && hidden.has(zone)) hiddenSectionIds.add(s.id);
417
+ }
418
+ const ungroupedHidden = !!ungrouped && hidden.has(UNGROUPED_ID);
419
+ for (const [objId, secId] of objectToSection) {
420
+ if (hiddenSectionIds.has(secId) || ungroupedHidden && secId === UNGROUPED_ID) out.add(objId);
421
+ }
422
+ return out;
423
+ }
424
+ function applyHidden(doc, hidden) {
425
+ if (!hidden.size) return doc;
426
+ const hide = hiddenObjectIds(doc, hidden);
427
+ const hiddenSet = hidden instanceof Set ? hidden : new Set(hidden);
428
+ const keep = (o) => {
429
+ if (hide.has(o.id)) return false;
430
+ if (o.type === "section" && isSectionHidden(o, hiddenSet)) return false;
431
+ return true;
432
+ };
433
+ if (doc.floors && doc.floors.length) {
434
+ let changed = false;
435
+ const floors = doc.floors.map((f) => {
436
+ const objects2 = f.objects.filter(keep);
437
+ if (objects2.length !== f.objects.length) changed = true;
438
+ return objects2.length === f.objects.length ? f : { ...f, objects: objects2 };
439
+ });
440
+ if (!changed) return doc;
441
+ return { ...doc, floors, objects: floors[0].objects };
442
+ }
443
+ const objects = doc.objects.filter(keep);
444
+ if (objects.length === doc.objects.length) return doc;
445
+ return { ...doc, objects };
446
+ }
447
+
304
448
  // src/engine/SeatmapRenderer.ts
305
449
  import { Konva } from "konva/lib/Core";
306
450
  import { Stage } from "konva/lib/Stage";
@@ -312,21 +456,162 @@ import { Ellipse } from "konva/lib/shapes/Ellipse";
312
456
  import { Line } from "konva/lib/shapes/Line";
313
457
  import { Text } from "konva/lib/shapes/Text";
314
458
  import { Image as KImage } from "konva/lib/shapes/Image";
459
+
460
+ // src/lib/money.ts
461
+ var DEFAULT_CURRENCY = "USD";
462
+ var displayLocale;
463
+ function setMoneyLocale(locale) {
464
+ displayLocale = locale;
465
+ }
466
+ var formatterCache = /* @__PURE__ */ new Map();
467
+ function formatter(currency, digits) {
468
+ const key = `${displayLocale ?? ""}|${currency}|${digits ?? "auto"}`;
469
+ let fmt = formatterCache.get(key);
470
+ if (!fmt) {
471
+ fmt = new Intl.NumberFormat(displayLocale, {
472
+ style: "currency",
473
+ currency,
474
+ minimumFractionDigits: digits,
475
+ maximumFractionDigits: digits
476
+ });
477
+ formatterCache.set(key, fmt);
478
+ }
479
+ return fmt;
480
+ }
481
+ function formatMoney(amount, currency = DEFAULT_CURRENCY, fractionDigits) {
482
+ const digits = fractionDigits ?? (Number.isInteger(amount) ? 0 : void 0);
483
+ return formatter(currency, digits).format(amount);
484
+ }
485
+ function currencySymbol(currency = DEFAULT_CURRENCY) {
486
+ const part = formatter(currency, 0).formatToParts(0).find((p) => p.type === "currency");
487
+ return part?.value ?? currency;
488
+ }
489
+
490
+ // src/i18n/locales/en.ts
491
+ var en = {
492
+ // shared
493
+ "common.cancel": "Cancel",
494
+ "common.close": "Close",
495
+ "common.done": "Done",
496
+ "common.copied": "\u2713 Copied",
497
+ // buyer picker (extraction in progress — P4 of the port plan)
498
+ "picker.holdSeats": "Hold seats & checkout",
499
+ "picker.completeBooking": "Complete booking",
500
+ "picker.seatsHeld": "Seats held \u2014 {time}",
501
+ "picker.holdExpired": "Your hold expired \u2014 the seats were released. Pick again.",
502
+ "picker.seatTaken": "Seat {label} was just taken by another buyer.",
503
+ "picker.poweredBy": "Powered by SeatMap",
504
+ "picker.seats.one": "{count} seat",
505
+ "picker.seats.other": "{count} seats",
506
+ // renderer (drawn on the Konva map — shared by the embed SDK)
507
+ "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
508
+ "map.seatsLeft": "{count} LEFT",
509
+ "map.fromPrice": "FROM {price}",
510
+ // buyer picker page (src/pages/PickerPage.tsx)
511
+ "picker.language": "Language",
512
+ "picker.zoomToFit": "Zoom to fit",
513
+ "picker.viewMode": "View mode",
514
+ "picker.floor": "Floor",
515
+ "picker.zoomLevel": "Zoom level",
516
+ "picker.rungTip.zones": "Farthest rung \u2014 zone blocks (Floor / Lower Bowl / Upper Bowl)",
517
+ "picker.rungTip.sections": "Section blocks \u2014 category-mix fill + availability tint",
518
+ "picker.rungTip.seats": "Individual seats \u2014 blocks melt into dots",
519
+ "picker.rungLabel.zones": "ZONES",
520
+ "picker.rungLabel.sections": "SECTIONS",
521
+ "picker.rungLabel.seats": "SEATS",
522
+ "picker.sectionSummaryAria": "{label} section summary",
523
+ "picker.closeSectionSummary": "Close section summary",
524
+ "picker.seatsLeftInSection.one": "{count} seat left",
525
+ "picker.seatsLeftInSection.other": "{count} seats left",
526
+ "picker.overview": "Overview",
527
+ "picker.tapSeatHint": "Tap any seat to check its view",
528
+ "picker.chartSize": "Chart size",
529
+ "picker.custom": "Custom",
530
+ "picker.categories": "Categories",
531
+ "picker.accessibility": "Accessibility",
532
+ "picker.showAnyAccessible": "Show any accessible seat",
533
+ "picker.any": "Any",
534
+ "picker.yourSeats": "Your seats",
535
+ "picker.emptySeats": "Tap seats on the map",
536
+ "picker.emptySeatsWithGa": "Tap seats on the map \xB7 tap a standing area for GA tickets",
537
+ "picker.oneFewer": "One fewer",
538
+ "picker.oneMore": "One more",
539
+ "picker.remove": "Remove {label}",
540
+ "picker.ticketTierFor": "Ticket tier for {label}",
541
+ "picker.total": "Total: {amount}",
542
+ "picker.viewFromSeat": "View from seat {label}",
543
+ "picker.real360": "REAL 360\xB0",
544
+ "picker.preview": "PREVIEW",
545
+ "picker.open360": "Open 360\xB0 view",
546
+ "picker.sightline": "\u2248 {rows} rows from stage \xB7 {angle}\xB0 off-center",
547
+ "picker.bookedDemo": "Booked! (demo)",
548
+ "picker.bookButton.one": "Book {count} ticket \u2014 {amount}",
549
+ "picker.bookButton.other": "Book {count} tickets \u2014 {amount}",
550
+ "picker.simulateCrowd": "Simulate crowd: {state}",
551
+ "picker.on": "ON",
552
+ "picker.off": "OFF",
553
+ "picker.panorama360": "360\xB0 venue photo",
554
+ "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m from stage"
555
+ };
556
+
557
+ // src/i18n/index.ts
558
+ var SUPPORTED_LOCALES = ["en", "es", "de", "fr"];
559
+ var bundles = { en };
560
+ var overrides = {};
561
+ var active = "en";
562
+ function resolveLocale(explicit, stored) {
563
+ for (const candidate of [explicit, stored, typeof navigator !== "undefined" ? navigator.language : null]) {
564
+ if (!candidate) continue;
565
+ const base = candidate.toLowerCase().split("-")[0];
566
+ if (SUPPORTED_LOCALES.includes(base)) return base;
567
+ }
568
+ return "en";
569
+ }
570
+ function getLocale() {
571
+ return active;
572
+ }
573
+ function setLocale(locale, bundle) {
574
+ if (bundle) bundles[locale] = { ...bundles[locale], ...bundle };
575
+ active = bundles[locale] ? locale : "en";
576
+ setMoneyLocale(active);
577
+ if (typeof document !== "undefined") document.documentElement.lang = active;
578
+ }
579
+ function setStringOverrides(next) {
580
+ overrides = next;
581
+ }
582
+ var PLACEHOLDER = /\{(\w+)\}/g;
583
+ function t(key, vars) {
584
+ const raw = overrides[key] ?? bundles[active]?.[key] ?? en[key] ?? key;
585
+ if (!vars) return raw;
586
+ return raw.replace(PLACEHOLDER, (_, name) => String(vars[name] ?? `{${name}}`));
587
+ }
588
+ function tCount(key, count, vars) {
589
+ const rule = new Intl.PluralRules(active).select(count);
590
+ const exact = overrides[`${key}.${rule}`] ?? bundles[active]?.[`${key}.${rule}`] ?? en[`${key}.${rule}`];
591
+ const raw = exact ?? overrides[`${key}.other`] ?? bundles[active]?.[`${key}.other`] ?? en[`${key}.other`] ?? key;
592
+ return raw.replace(PLACEHOLDER, (_, name) => String({ count, ...vars }[name] ?? `{${name}}`));
593
+ }
594
+ function formatDate(value, opts) {
595
+ return new Intl.DateTimeFormat(active, opts ?? { dateStyle: "medium", timeStyle: "short" }).format(value);
596
+ }
597
+
598
+ // src/engine/SeatmapRenderer.ts
315
599
  var SEAT_RADIUS = 9;
316
600
  var SEAT_LEGIBLE_SCALE = 0.9;
317
601
  var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
318
602
  var LABEL_SCALE = 1;
319
- var SECTION_PROMINENT_SCALE = 0.35 * SEAT_LEGIBLE_SCALE;
603
+ var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
604
+ var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
320
605
  var ZONE_PROMINENT_SCALE = 0.55 * SECTION_PROMINENT_SCALE;
321
606
  var MAX_LABELS = 700;
322
607
  var ISO_ANGLE_DEG = -11.5;
323
608
  var ISO_SQUASH = 0.58;
324
- var LIFT_PER_STEP = 46;
609
+ var LIFT_PER_STEP = 58;
325
610
  var ISO_TWEEN_MS = 320;
326
611
  var BLOCK_FILL_ALPHA = 0.85;
327
612
  var SOLD_DARKEN = 0.5;
328
- var SECTION_LABEL_PX = 18;
329
- var SECTION_SUB_PX = 11;
613
+ var SECTION_LABEL_PX = 20;
614
+ var SECTION_SUB_PX = 12.5;
330
615
  var ZONE_LABEL_PX = 30;
331
616
  var ZONE_SUB_PX = 12;
332
617
  var HELD_FILL = "#6b7280";
@@ -343,7 +628,7 @@ var ACCESS_RING = {
343
628
  };
344
629
  function seatMatchesAccess(seat, filter) {
345
630
  if (filter.length === 0) return !!seat.accessible;
346
- return !!seat.accessibility?.some((t) => filter.includes(t));
631
+ return !!seat.accessibility?.some((t2) => filter.includes(t2));
347
632
  }
348
633
  var DEF_SEAT_LABEL = "#0b1220";
349
634
  var DEF_SELECTION = "#ffffff";
@@ -412,16 +697,21 @@ function mixColors(parts, fallback) {
412
697
  }
413
698
  return tw > 0 ? toHex(r / tw, g / tw, b / tw) : fallback;
414
699
  }
415
- function lerpColor(a, b, t) {
700
+ function lerpColor(a, b, t2) {
416
701
  const ca = hexToRgb(a);
417
702
  const cb = hexToRgb(b);
418
703
  if (!ca || !cb) return a;
419
- return toHex(ca[0] + (cb[0] - ca[0]) * t, ca[1] + (cb[1] - ca[1]) * t, ca[2] + (cb[2] - ca[2]) * t);
704
+ return toHex(ca[0] + (cb[0] - ca[0]) * t2, ca[1] + (cb[1] - ca[1]) * t2, ca[2] + (cb[2] - ca[2]) * t2);
420
705
  }
421
706
  var _SeatmapRenderer = class _SeatmapRenderer {
422
707
  constructor(container, options = {}) {
423
708
  this.seats = [];
424
709
  this.seatById = /* @__PURE__ */ new Map();
710
+ /** Multi-floor (Batch 5): the last-set chart + which floor we're rendering. */
711
+ this.chartDoc = null;
712
+ this.activeFloorId = "";
713
+ /** When true on a multi-floor chart, render ALL floors stacked (3D overview). */
714
+ this.stacked = false;
425
715
  /** Interactive node per seat/booth — a Circle for seats, a Rect for booths. */
426
716
  this.circleById = /* @__PURE__ */ new Map();
427
717
  /** Booth block geometry, keyed by booth id (= the unit's rowId). */
@@ -445,6 +735,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
445
735
  this.zones = [];
446
736
  this.seatSection = /* @__PURE__ */ new Map();
447
737
  this.catPrice = /* @__PURE__ */ new Map();
738
+ /** Section/zone ids to render dimmed (organizer manager: held-back inventory). */
739
+ this.dimmedSections = /* @__PURE__ */ new Set();
740
+ /** Object id → floor id (multi-floor only) — resolves a deck tap in the 3D stack. */
741
+ this.objectFloor = /* @__PURE__ */ new Map();
448
742
  /** Zone id → colour (drives extruded side faces in iso view). */
449
743
  this.zoneColor = /* @__PURE__ */ new Map();
450
744
  this.hasSections = false;
@@ -457,6 +751,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
457
751
  this.isoRaf = 0;
458
752
  /** Chart centre the iso projection pivots about (bounds centre). */
459
753
  this.isoCentre = { x: 0, y: 0 };
754
+ /** rAF for an in-flight camera glide (focusRegion / setRung); 0 = none. */
755
+ this.glideRaf = 0;
460
756
  /** Set in destroy() so an in-flight iso tween bails. */
461
757
  this.destroyed = false;
462
758
  /** Cached scale the section/zone labels were last sized for (scale-compensation). */
@@ -504,6 +800,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
504
800
  // pointer events away from Konva's canvas, killing its click/tap/hover
505
801
  // pipeline. We rely on bubbling instead.
506
802
  this.onPointerDown = (e) => {
803
+ this.cancelGlide();
507
804
  this.pointers.set(e.pointerId, this.toLocal(e));
508
805
  if (this.pointers.size === 1) {
509
806
  this.moved = 0;
@@ -565,6 +862,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
565
862
  };
566
863
  this.container = container;
567
864
  this.opts = { maxSelection: 10, selectableStatuses: ["free"], ...options };
865
+ this.currency = options.currency;
568
866
  Konva.pixelRatio = this.dpr;
569
867
  this.stage = new Stage({
570
868
  container,
@@ -581,7 +879,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
581
879
  if (container.tabIndex < 0) container.tabIndex = 0;
582
880
  container.setAttribute("role", "application");
583
881
  if (!container.getAttribute("aria-label")) {
584
- container.setAttribute("aria-label", "Seating map. Use arrow keys to move between seats, Enter to select.");
882
+ container.setAttribute("aria-label", t("map.aria"));
585
883
  }
586
884
  container.addEventListener("keydown", this.onKeyDown);
587
885
  this.bgLayer = new Layer({ listening: true });
@@ -624,7 +922,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
624
922
  }
625
923
  }
626
924
  // ---- ISeatmapRenderer -----------------------------------------------------
627
- setChart(doc) {
925
+ setChart(doc, opts) {
926
+ if (doc !== this.chartDoc) this.stacked = false;
927
+ this.chartDoc = doc;
928
+ this.activeFloorId = opts?.floorId ?? floorsOf(doc)[0].id;
929
+ this.objectFloor.clear();
930
+ if (doc.floors && doc.floors.length > 1) {
931
+ for (const f of doc.floors) for (const o of f.objects) this.objectFloor.set(o.id, f.id);
932
+ }
933
+ const view = this.floorView(doc);
628
934
  this.focusedId = null;
629
935
  this.focusRing.visible(false);
630
936
  this.bgLayer.destroyChildren();
@@ -652,7 +958,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
652
958
  this.isoT = 0;
653
959
  this.isoTarget = 0;
654
960
  this.resetLayerTransforms();
655
- this.hasSections = doc.objects.some((o) => o.type === "section");
961
+ this.hasSections = view.objects.some((o) => o.type === "section");
656
962
  for (const z of doc.zones ?? []) if (z.color) this.zoneColor.set(z.id, z.color);
657
963
  this.seatLayer.opacity(1);
658
964
  this.hoverRing.visible(false);
@@ -667,24 +973,56 @@ var _SeatmapRenderer = class _SeatmapRenderer {
667
973
  this.catColor.set(c.key, c.color);
668
974
  if (typeof c.price === "number") this.catPrice.set(c.key, c.price);
669
975
  }
670
- for (const obj of doc.objects) {
976
+ for (const obj of view.objects) {
671
977
  if (obj.type === "booth") {
672
978
  this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
673
979
  }
674
980
  }
675
- this.seats = expandChart(doc);
981
+ this.seats = expandChart(view);
676
982
  for (const s of this.seats) {
677
983
  this.seatById.set(s.id, s);
678
984
  this.statusById.set(s.id, "free");
679
985
  }
680
- this.renderBackground(doc);
986
+ this.renderBackground(view);
681
987
  this.renderSeats();
682
988
  this.overlayLayer.add(this.labelGroup);
683
989
  this.overlayLayer.add(this.hoverRing);
684
- this.bounds = chartBounds(doc);
990
+ this.bounds = chartBounds(view);
685
991
  this.isoCentre = { x: this.bounds.x + this.bounds.width / 2, y: this.bounds.y + this.bounds.height / 2 };
686
992
  this.zoomToFit();
687
993
  }
994
+ /** The chart to render: all floors stacked (3D overview), the active floor, or
995
+ * the whole chart for single-floor charts. */
996
+ floorView(doc) {
997
+ if (!doc.floors || !doc.floors.length) return doc;
998
+ if (this.stacked && doc.floors.length >= 2) return stackFloors(doc);
999
+ const floor = doc.floors.find((f) => f.id === this.activeFloorId) ?? doc.floors[0];
1000
+ return { ...doc, objects: floor.objects, focalPoint: floor.focalPoint, backgroundImage: floor.backgroundImage, floors: void 0 };
1001
+ }
1002
+ /** Toggle the 3D all-floors stacked overview (Batch 5). Re-renders; no-op on
1003
+ * single-floor charts. The caller re-applies statuses + animates the iso view. */
1004
+ setStacked(on) {
1005
+ if (!this.chartDoc || on === this.stacked) return;
1006
+ const multi = !!this.chartDoc.floors && this.chartDoc.floors.length >= 2;
1007
+ if (!multi) return;
1008
+ this.stacked = on;
1009
+ this.setChart(this.chartDoc, { floorId: this.activeFloorId });
1010
+ }
1011
+ isStacked() {
1012
+ return this.stacked;
1013
+ }
1014
+ /** Switch which floor is shown (2D). Re-renders + re-fits; no-op if unchanged. */
1015
+ setActiveFloor(floorId) {
1016
+ if (!this.chartDoc || floorId === this.activeFloorId) return;
1017
+ this.setChart(this.chartDoc, { floorId });
1018
+ }
1019
+ /** Floors for the host's switcher (single-floor charts return one synthetic floor). */
1020
+ getFloors() {
1021
+ return this.chartDoc ? floorsOf(this.chartDoc).map((f) => ({ id: f.id, name: f.name })) : [];
1022
+ }
1023
+ getActiveFloorId() {
1024
+ return this.activeFloorId;
1025
+ }
688
1026
  setStatus(seatIds, status) {
689
1027
  let touched = false;
690
1028
  const affected = this.hasSections ? /* @__PURE__ */ new Set() : null;
@@ -764,11 +1102,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
764
1102
  const dur = 620;
765
1103
  const step = (now) => {
766
1104
  if (!ring.getLayer()) return;
767
- const t = Math.min(1, (now - start) / dur);
768
- ring.radius(this.seatR * (1 + t * 1.8));
769
- ring.opacity(0.9 * (1 - t));
1105
+ const t2 = Math.min(1, (now - start) / dur);
1106
+ ring.radius(this.seatR * (1 + t2 * 1.8));
1107
+ ring.opacity(0.9 * (1 - t2));
770
1108
  this.overlayLayer.batchDraw();
771
- if (t < 1) requestAnimationFrame(step);
1109
+ if (t2 < 1) requestAnimationFrame(step);
772
1110
  else {
773
1111
  ring.destroy();
774
1112
  this.overlayLayer.batchDraw();
@@ -875,7 +1213,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
875
1213
  sameAccessFilter(next) {
876
1214
  const cur = this.accessFilter;
877
1215
  if (cur === null || next === null) return cur === next;
878
- return cur.length === next.length && cur.every((t, i) => t === next[i]);
1216
+ return cur.length === next.length && cur.every((t2, i) => t2 === next[i]);
879
1217
  }
880
1218
  // ---- isometric ("3D") view mode -------------------------------------------
881
1219
  /**
@@ -923,6 +1261,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
923
1261
  };
924
1262
  this.isoRaf = requestAnimationFrame(step);
925
1263
  }
1264
+ /** Current projection — reflects the tween target, not the mid-tween isoT. */
1265
+ getViewMode() {
1266
+ return this.isoTarget === 1 ? "isometric" : "flat";
1267
+ }
926
1268
  /** Iso angle (rad) + y-squash for the current isoT. */
927
1269
  isoParams() {
928
1270
  return { th: ISO_ANGLE_DEG * Math.PI / 180 * this.isoT, sg: 1 - (1 - ISO_SQUASH) * this.isoT };
@@ -980,8 +1322,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
980
1322
  * lift + extruded side faces on elevated sections.
981
1323
  */
982
1324
  applyIso() {
983
- const t = this.isoT;
984
- if (t === 0) {
1325
+ const t2 = this.isoT;
1326
+ if (t2 === 0) {
985
1327
  this.resetLayerTransforms();
986
1328
  } else {
987
1329
  const { th, sg } = this.isoParams();
@@ -1034,13 +1376,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1034
1376
  }
1035
1377
  /** Lift elevated sections (+ their members) and update their extruded side faces. */
1036
1378
  applyElevation() {
1037
- const t = this.isoT;
1379
+ const t2 = this.isoT;
1038
1380
  for (const sec of this.sections) {
1039
1381
  if (sec.elevation <= 0) continue;
1040
1382
  const off = this.isoLiftLocal(sec.elevation);
1041
1383
  sec.liftGroupBg?.position(off);
1042
1384
  sec.liftGroupSeat?.position(off);
1043
- const alpha = 0.9 * t;
1385
+ const alpha = 0.9 * t2;
1044
1386
  for (let i = 0; i < sec.sideFaces.length; i++) {
1045
1387
  const face = sec.sideFaces[i];
1046
1388
  const p0 = sec.outline[i];
@@ -1054,6 +1396,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1054
1396
  this.destroyed = true;
1055
1397
  if (this.rafId) cancelAnimationFrame(this.rafId);
1056
1398
  if (this.isoRaf) cancelAnimationFrame(this.isoRaf);
1399
+ if (this.glideRaf) cancelAnimationFrame(this.glideRaf);
1057
1400
  if (this.viewChangeRaf) cancelAnimationFrame(this.viewChangeRaf);
1058
1401
  if (this.recacheTimer) clearTimeout(this.recacheTimer);
1059
1402
  this.resizeObs?.disconnect();
@@ -1133,7 +1476,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1133
1476
  this.circleById.set(seat.id, rect);
1134
1477
  this.paintSeat(rect, seat.id);
1135
1478
  target.add(rect);
1136
- const t = new Text({
1479
+ const t2 = new Text({
1137
1480
  x: seat.x,
1138
1481
  y: seat.y,
1139
1482
  text: seat.label,
@@ -1144,10 +1487,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1144
1487
  listening: false,
1145
1488
  perfectDrawEnabled: false
1146
1489
  });
1147
- t.offsetX(t.width() / 2);
1148
- t.offsetY(t.height() / 2);
1490
+ t2.offsetX(t2.width() / 2);
1491
+ t2.offsetY(t2.height() / 2);
1149
1492
  this.hasBoothText = true;
1150
- target.add(t);
1493
+ target.add(t2);
1151
1494
  }
1152
1495
  /** Apply fill/stroke/opacity for a seat's current status + selection. */
1153
1496
  paintSeat(c, id) {
@@ -1183,6 +1526,32 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1183
1526
  if (this.categoryHighlight && status === "free" && !selected && seat.categoryKey !== this.categoryHighlight) {
1184
1527
  c.opacity(0.25);
1185
1528
  }
1529
+ if (this.dimmedSections.size) {
1530
+ const sec = this.seatSection.get(id);
1531
+ if (sec && (this.dimmedSections.has(sec.id) || sec.zone != null && this.dimmedSections.has(sec.zone))) {
1532
+ c.opacity(0.18);
1533
+ }
1534
+ }
1535
+ }
1536
+ /**
1537
+ * Dim the seats of these section/zone ids (organizer manager use) so held-back
1538
+ * inventory is visually distinct on the canvas without hiding it. `null`/empty
1539
+ * clears. Unlike the buyer's applyHidden, the sections stay rendered.
1540
+ */
1541
+ setDimmedSections(ids) {
1542
+ const next = new Set(ids ?? []);
1543
+ if (next.size === this.dimmedSections.size && [...next].every((i) => this.dimmedSections.has(i))) return;
1544
+ this.dimmedSections = next;
1545
+ for (const seat of this.seats) {
1546
+ const c = this.circleById.get(seat.id);
1547
+ if (c) this.paintSeat(c, seat.id);
1548
+ }
1549
+ if (this.cached) {
1550
+ this.seatLayer.clearCache();
1551
+ this.cacheSeatLayer();
1552
+ } else {
1553
+ this.seatLayer.batchDraw();
1554
+ }
1186
1555
  }
1187
1556
  /** Legend hover: highlight one category (dim the rest), or null to clear. */
1188
1557
  setCategoryHighlight(key) {
@@ -1365,7 +1734,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1365
1734
  }
1366
1735
  /** Prominent stage caption: uppercase, letter-spaced, larger, softly dimmed. */
1367
1736
  addStageLabel(x, y, text) {
1368
- const t = new Text({
1737
+ const t2 = new Text({
1369
1738
  x,
1370
1739
  y,
1371
1740
  text: text.toUpperCase(),
@@ -1377,9 +1746,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1377
1746
  listening: false,
1378
1747
  perfectDrawEnabled: false
1379
1748
  });
1380
- t.offsetX(t.width() / 2);
1381
- t.offsetY(t.height() / 2);
1382
- this.bgLayer.add(t);
1749
+ t2.offsetX(t2.width() / 2);
1750
+ t2.offsetY(t2.height() / 2);
1751
+ this.bgLayer.add(t2);
1383
1752
  }
1384
1753
  renderGA(obj) {
1385
1754
  const color = this.catColor.get(obj.categoryKey) ?? "#6e7bff";
@@ -1460,12 +1829,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1460
1829
  this.seatLayer.add(liftGroupSeat);
1461
1830
  }
1462
1831
  const bgTarget = liftGroupBg ?? this.bgLayer;
1832
+ const outlineTint = obj.color ?? this.zoneColor.get(obj.zone ?? "") ?? "#3a4358";
1463
1833
  const outlinePoly = new Line({
1464
1834
  points: pts,
1465
1835
  closed: true,
1466
- stroke: "#3a4358",
1467
- strokeWidth: 1.5,
1468
- fill: rgba(obj.color ?? "#3a4358", 0.06),
1836
+ stroke: rgba(outlineTint, 0.5),
1837
+ strokeWidth: 1.75,
1838
+ fill: rgba(outlineTint, 0.08),
1839
+ lineJoin: "round",
1469
1840
  listening: false,
1470
1841
  perfectDrawEnabled: false
1471
1842
  });
@@ -1481,6 +1852,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1481
1852
  perfectDrawEnabled: false
1482
1853
  });
1483
1854
  bgTarget.add(blockPoly);
1855
+ const rowSeats = /* @__PURE__ */ new Map();
1856
+ for (const id of memberIds) {
1857
+ const s = this.seatById.get(id);
1858
+ if (!s) continue;
1859
+ const i = Number(id.slice(id.lastIndexOf(":") + 1)) || 0;
1860
+ (rowSeats.get(s.rowId) ?? rowSeats.set(s.rowId, []).get(s.rowId)).push({ i, x: s.x, y: s.y });
1861
+ }
1862
+ const rowLines = [];
1863
+ for (const arr of rowSeats.values()) {
1864
+ if (arr.length < 2) continue;
1865
+ arr.sort((a, b) => a.i - b.i);
1866
+ const line = new Line({
1867
+ points: arr.flatMap((p) => [p.x, p.y]),
1868
+ stroke: rgba("#ffffff", 0.34),
1869
+ strokeWidth: SEAT_RADIUS * 0.55,
1870
+ lineCap: "round",
1871
+ lineJoin: "round",
1872
+ opacity: 0,
1873
+ listening: false,
1874
+ perfectDrawEnabled: false
1875
+ });
1876
+ rowLines.push(line);
1877
+ bgTarget.add(line);
1878
+ }
1484
1879
  const nameLabel = new Text({
1485
1880
  x: centroid.x,
1486
1881
  y: centroid.y,
@@ -1489,6 +1884,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1489
1884
  fontStyle: "700",
1490
1885
  fontFamily: this.labelFont(),
1491
1886
  fill: "#8b93a7",
1887
+ // Dark halo so the label reads over the seat dots at any zoom.
1888
+ shadowColor: "#05070c",
1889
+ shadowBlur: 6,
1890
+ shadowOpacity: 0.9,
1891
+ shadowForStrokeEnabled: false,
1492
1892
  listening: false,
1493
1893
  perfectDrawEnabled: false
1494
1894
  });
@@ -1498,11 +1898,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1498
1898
  const subLabel = new Text({
1499
1899
  x: centroid.x,
1500
1900
  y: centroid.y,
1501
- text: `${free} LEFT`,
1901
+ text: t("map.seatsLeft", { count: free }),
1502
1902
  fontSize: 12,
1503
- fontStyle: "600",
1903
+ fontStyle: "700",
1504
1904
  fontFamily: "JetBrains Mono, ui-monospace, monospace",
1505
- fill: rgba("#e6e9f0", 0.8),
1905
+ fill: "#f4f6fb",
1906
+ shadowColor: "#05070c",
1907
+ shadowBlur: 5,
1908
+ shadowOpacity: 0.9,
1909
+ shadowForStrokeEnabled: false,
1506
1910
  opacity: 0,
1507
1911
  listening: false,
1508
1912
  perfectDrawEnabled: false
@@ -1521,6 +1925,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1521
1925
  baseFill,
1522
1926
  outlinePoly,
1523
1927
  blockPoly,
1928
+ rowLines,
1524
1929
  nameLabel,
1525
1930
  subLabel,
1526
1931
  elevation,
@@ -1536,7 +1941,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1536
1941
  refreshSectionFill(sec) {
1537
1942
  const sold = sec.total > 0 ? (sec.total - sec.free) / sec.total : 0;
1538
1943
  sec.blockPoly.fill(darken(sec.baseFill, sold * SOLD_DARKEN));
1539
- sec.subLabel.text(`${sec.free} LEFT`);
1944
+ sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
1540
1945
  sec.subLabel.offsetX(sec.subLabel.width() / 2);
1541
1946
  }
1542
1947
  /**
@@ -1554,8 +1959,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1554
1959
  for (const z of doc.zones) {
1555
1960
  const members = byZone.get(z.id);
1556
1961
  if (!members || !members.length) continue;
1557
- const cx = members.reduce((a, s) => a + s.centroid.x, 0) / members.length;
1558
- const cy = members.reduce((a, s) => a + s.centroid.y, 0) / members.length;
1962
+ let anchor = members[0];
1963
+ for (const s of members) if (s.centroid.y < anchor.centroid.y) anchor = s;
1964
+ const cx = anchor.centroid.x;
1965
+ const cy = anchor.centroid.y;
1559
1966
  let minPrice = Infinity;
1560
1967
  for (const sec of members) {
1561
1968
  for (const id of sec.memberIds) {
@@ -1573,6 +1980,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1573
1980
  letterSpacing: 2,
1574
1981
  fontFamily: this.labelFont(),
1575
1982
  fill: z.color ?? "#f2f4f8",
1983
+ shadowColor: "#05070c",
1984
+ shadowBlur: 10,
1985
+ shadowOpacity: 0.95,
1986
+ shadowForStrokeEnabled: false,
1576
1987
  opacity: 0,
1577
1988
  listening: false,
1578
1989
  perfectDrawEnabled: false
@@ -1585,7 +1996,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1585
1996
  sub = new Text({
1586
1997
  x: cx,
1587
1998
  y: cy,
1588
- text: `FROM $${minPrice}`,
1999
+ text: t("map.fromPrice", { price: formatMoney(minPrice, this.currency) }),
1589
2000
  fontSize: 14,
1590
2001
  fontStyle: "600",
1591
2002
  fontFamily: "JetBrains Mono, ui-monospace, monospace",
@@ -1616,7 +2027,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1616
2027
  blockT = scale <= SECTION_PROMINENT_SCALE ? 1 : 0;
1617
2028
  zoneT = scale <= ZONE_PROMINENT_SCALE ? 1 : 0;
1618
2029
  } else {
1619
- blockT = clamp((CACHE_THRESHOLD - scale) / (CACHE_THRESHOLD - SECTION_PROMINENT_SCALE), 0, 1);
2030
+ blockT = clamp((BLOCK_MELT_TOP - scale) / (BLOCK_MELT_TOP - SECTION_PROMINENT_SCALE), 0, 1);
1620
2031
  zoneT = clamp((SECTION_PROMINENT_SCALE - scale) / (SECTION_PROMINENT_SCALE - ZONE_PROMINENT_SCALE), 0, 1);
1621
2032
  }
1622
2033
  if (!this.zones.length) zoneT = 0;
@@ -1626,7 +2037,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1626
2037
  if (rescale) this.lodScale = scale;
1627
2038
  for (const sec of this.sections) {
1628
2039
  sec.blockPoly.opacity(BLOCK_FILL_ALPHA * blockT);
1629
- sec.nameLabel.fill(lerpColor("#8b93a7", "#f2f4f8", blockT));
2040
+ for (const line of sec.rowLines) line.opacity(blockT * (1 - zoneT));
2041
+ sec.nameLabel.fill(lerpColor("#aab3c5", "#ffffff", blockT));
1630
2042
  sec.nameLabel.opacity(1 - zoneT);
1631
2043
  sec.subLabel.opacity(blockT * (1 - zoneT));
1632
2044
  if (rescale) {
@@ -1634,9 +2046,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1634
2046
  this.sizeLabel(sec.subLabel, SECTION_SUB_PX / sx, sec.centroid.y + SECTION_LABEL_PX / sx);
1635
2047
  }
1636
2048
  }
2049
+ const zoneOpacity = zoneT * (1 - this.isoT);
1637
2050
  for (const zone of this.zones) {
1638
- zone.label.opacity(zoneT);
1639
- if (zone.sub) zone.sub.opacity(zoneT);
2051
+ zone.label.opacity(zoneOpacity);
2052
+ if (zone.sub) zone.sub.opacity(zoneOpacity);
1640
2053
  if (rescale) {
1641
2054
  const cy = zone.label.y();
1642
2055
  this.sizeLabel(zone.label, ZONE_LABEL_PX / sx, cy);
@@ -1646,11 +2059,11 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1646
2059
  this.bgLayer.batchDraw();
1647
2060
  }
1648
2061
  /** Set a centred label's world fontSize (for a target screen px) and re-anchor it. */
1649
- sizeLabel(t, fontSize, y) {
1650
- t.fontSize(Math.max(1, fontSize));
1651
- t.offsetX(t.width() / 2);
1652
- t.offsetY(t.height() / 2);
1653
- t.y(y);
2062
+ sizeLabel(t2, fontSize, y) {
2063
+ t2.fontSize(Math.max(1, fontSize));
2064
+ t2.offsetX(t2.width() / 2);
2065
+ t2.offsetY(t2.height() / 2);
2066
+ t2.y(y);
1654
2067
  }
1655
2068
  /**
1656
2069
  * Map a container-relative screen point back to world coords. Inverts the
@@ -1674,7 +2087,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1674
2087
  return this.sections.find((s) => s.id === id)?.memberIds.slice() ?? [];
1675
2088
  }
1676
2089
  addCentredLabel(layer, text, x, y, fill, fontSize, bold) {
1677
- const t = new Text({
2090
+ const t2 = new Text({
1678
2091
  x,
1679
2092
  y,
1680
2093
  text,
@@ -1685,9 +2098,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1685
2098
  listening: false,
1686
2099
  perfectDrawEnabled: false
1687
2100
  });
1688
- t.offsetX(t.width() / 2);
1689
- t.offsetY(t.height() / 2);
1690
- layer.add(t);
2101
+ t2.offsetX(t2.width() / 2);
2102
+ t2.offsetY(t2.height() / 2);
2103
+ layer.add(t2);
1691
2104
  }
1692
2105
  // ---- selection ------------------------------------------------------------
1693
2106
  isSelectable(id) {
@@ -1741,7 +2154,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1741
2154
  this.seatLayer.on("click tap", (e) => {
1742
2155
  if (this.moved > 8) return;
1743
2156
  const id = seatIdOf(e.target);
1744
- if (id) this.toggleSeat(id);
2157
+ if (!id) return;
2158
+ if (this.stacked && this.opts.onDeckTap) {
2159
+ const floorId = this.objectFloor.get(this.seatById.get(id)?.rowId ?? "");
2160
+ if (floorId) {
2161
+ this.opts.onDeckTap(floorId);
2162
+ return;
2163
+ }
2164
+ }
2165
+ this.toggleSeat(id);
1745
2166
  });
1746
2167
  this.seatLayer.on("mouseover", (e) => {
1747
2168
  const id = seatIdOf(e.target);
@@ -1771,11 +2192,19 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1771
2192
  if (!this.cached || this.moved > 8) return;
1772
2193
  const pointer = this.stage.getPointerPosition();
1773
2194
  if (!pointer) return;
2195
+ if (this.stacked && this.opts.onDeckTap) {
2196
+ const floorId = this.deckFloorAt();
2197
+ if (floorId) {
2198
+ this.opts.onDeckTap(floorId);
2199
+ return;
2200
+ }
2201
+ }
1774
2202
  if (this.sections.length) {
1775
2203
  const world = this.screenToWorld(pointer);
1776
2204
  const hit = this.sections.find((sn) => pointInPolygon(world, sn.outline));
1777
2205
  if (hit) {
1778
- this.zoomToBounds(polyBounds(hit.outline));
2206
+ if (this.opts.onSectionTap) this.opts.onSectionTap(hit.id);
2207
+ else this.focusRegion(hit.id);
1779
2208
  return;
1780
2209
  }
1781
2210
  }
@@ -1783,6 +2212,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1783
2212
  this.zoomAbout(target, pointer);
1784
2213
  });
1785
2214
  }
2215
+ /**
2216
+ * Which deck (floor) a screen tap landed on in the 3D stacked overview. The
2217
+ * seat layer is melt-cached at that zoom so individual seat nodes aren't
2218
+ * hit-testable; instead we invert the iso+stage transform via the layer's
2219
+ * relative pointer and take the nearest seat's floor. Only the floor matters,
2220
+ * so within-floor elevation offsets don't affect the result. Null if none.
2221
+ */
2222
+ deckFloorAt() {
2223
+ if (!this.objectFloor.size) return null;
2224
+ const p = this.seatLayer.getRelativePointerPosition();
2225
+ if (!p) return null;
2226
+ let best = null;
2227
+ let bestD = Infinity;
2228
+ for (const s of this.seats) {
2229
+ const dx = s.x - p.x;
2230
+ const dy = s.y - p.y;
2231
+ const d = dx * dx + dy * dy;
2232
+ if (d < bestD) {
2233
+ bestD = d;
2234
+ best = s.rowId;
2235
+ }
2236
+ }
2237
+ return best ? this.objectFloor.get(best) ?? null : null;
2238
+ }
1786
2239
  // ---- Pan & pinch (raw pointer events — mouse, touch and pen alike) --------
1787
2240
  toLocal(e) {
1788
2241
  const r = this.container.getBoundingClientRect();
@@ -1793,7 +2246,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1793
2246
  * reach a readable size on any chart, however large the venue.
1794
2247
  */
1795
2248
  zoomBounds() {
1796
- return { min: 0.5 * this.fitScale, max: Math.max(10 * this.fitScale, 4) };
2249
+ let min = 0.5 * this.fitScale;
2250
+ if (this.zones.length) min = Math.min(min, ZONE_PROMINENT_SCALE * 0.9);
2251
+ return { min, max: Math.max(10 * this.fitScale, 4) };
1797
2252
  }
1798
2253
  /** Zoom so `clientPoint` (stage-relative px) stays fixed under the cursor. */
1799
2254
  zoomAbout(nextScale, clientPoint) {
@@ -1826,6 +2281,92 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1826
2281
  this.afterViewChange();
1827
2282
  this.stage.batchDraw();
1828
2283
  }
2284
+ /**
2285
+ * Smoothly glide the camera to frame a section (by id) or a world-space bounds
2286
+ * rect — the Slice 5 "glide in". Pan+zoom tween over ~450ms easeInOutCubic; the
2287
+ * melt/LOD rides the camera every frame. `prefers-reduced-motion` (or
2288
+ * `opts.animate === false`) snaps via zoomToBounds. A grab (pointer-down) or a
2289
+ * newer glide cancels an in-flight one.
2290
+ */
2291
+ focusRegion(target, opts) {
2292
+ const b = typeof target === "string" ? (() => {
2293
+ const sec = this.sections.find((s) => s.id === target);
2294
+ return sec ? polyBounds(sec.outline) : null;
2295
+ })() : target;
2296
+ if (!b) return;
2297
+ this.cancelGlide();
2298
+ if (opts?.animate === false || this.reducedMotion) {
2299
+ this.zoomToBounds(b);
2300
+ return;
2301
+ }
2302
+ const w = this.stage.width();
2303
+ const h = this.stage.height();
2304
+ const { min, max } = this.zoomBounds();
2305
+ const margin = 1.12;
2306
+ const toScale = clamp(Math.min(w / (b.width * margin), h / (b.height * margin)), min, max);
2307
+ const toX = w / 2 - (b.x + b.width / 2) * toScale;
2308
+ const toY = h / 2 - (b.y + b.height / 2) * toScale;
2309
+ const fromScale = this.stage.scaleX();
2310
+ const fromX = this.stage.x();
2311
+ const fromY = this.stage.y();
2312
+ if (Math.abs(toScale - fromScale) < 1e-4 && Math.abs(toX - fromX) < 0.5 && Math.abs(toY - fromY) < 0.5) {
2313
+ this.afterViewChange();
2314
+ return;
2315
+ }
2316
+ const start = performance.now();
2317
+ const DUR = 450;
2318
+ const step = (now) => {
2319
+ if (this.destroyed) {
2320
+ this.glideRaf = 0;
2321
+ return;
2322
+ }
2323
+ const raw = Math.min(1, (now - start) / DUR);
2324
+ const e = raw < 0.5 ? 4 * raw * raw * raw : 1 - Math.pow(-2 * raw + 2, 3) / 2;
2325
+ const sc = fromScale + (toScale - fromScale) * e;
2326
+ this.stage.scale({ x: sc, y: sc });
2327
+ this.stage.position({ x: fromX + (toX - fromX) * e, y: fromY + (toY - fromY) * e });
2328
+ this.updateLOD();
2329
+ this.scheduleViewChange();
2330
+ this.stage.batchDraw();
2331
+ if (raw < 1) {
2332
+ this.glideRaf = requestAnimationFrame(step);
2333
+ } else {
2334
+ this.glideRaf = 0;
2335
+ this.afterViewChange();
2336
+ }
2337
+ };
2338
+ this.glideRaf = requestAnimationFrame(step);
2339
+ }
2340
+ /** Cancel an in-flight camera glide (a grab or a newer glide interrupts it). */
2341
+ cancelGlide() {
2342
+ if (this.glideRaf) {
2343
+ cancelAnimationFrame(this.glideRaf);
2344
+ this.glideRaf = 0;
2345
+ }
2346
+ }
2347
+ /** Current LOD rung derived from effective zoom — drives the ZONES/SECTIONS/SEATS pill. */
2348
+ getRung() {
2349
+ const scale = this.effScale();
2350
+ if (scale >= CACHE_THRESHOLD) return "seats";
2351
+ if (this.zones.length && scale < ZONE_PROMINENT_SCALE) return "zones";
2352
+ return "sections";
2353
+ }
2354
+ /** Jump the camera to a rung's zoom band, centred on the chart (glided). */
2355
+ setRung(rung) {
2356
+ if (rung === "zones") {
2357
+ this.cancelGlide();
2358
+ this.zoomToFit();
2359
+ return;
2360
+ }
2361
+ const target = rung === "sections" ? (SECTION_PROMINENT_SCALE + CACHE_THRESHOLD) / 2 : Math.max(SEAT_LEGIBLE_SCALE * 1.1, CACHE_THRESHOLD * 1.3);
2362
+ const w = this.stage.width();
2363
+ const h = this.stage.height();
2364
+ const cx = this.bounds.x + this.bounds.width / 2;
2365
+ const cy = this.bounds.y + this.bounds.height / 2;
2366
+ const bw = w / (target * 1.12);
2367
+ const bh = h / (target * 1.12);
2368
+ this.focusRegion({ x: cx - bw / 2, y: cy - bh / 2, width: bw, height: bh });
2369
+ }
1829
2370
  /** Recompute LOD (cache/labels) after any pan/zoom settles. */
1830
2371
  afterViewChange() {
1831
2372
  this.updateLOD();
@@ -1876,7 +2417,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1876
2417
  let count = 0;
1877
2418
  for (const seat of this.seats) {
1878
2419
  if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
1879
- const t = new Text({
2420
+ const t2 = new Text({
1880
2421
  x: seat.x,
1881
2422
  y: seat.y,
1882
2423
  text: seat.label,
@@ -1888,14 +2429,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1888
2429
  perfectDrawEnabled: false
1889
2430
  });
1890
2431
  const maxW = this.seatR * 2 - 3;
1891
- if (t.width() > maxW) t.fontSize(Math.max(4, 7 * maxW / t.width()));
1892
- if (t.fontSize() < 4.2) {
1893
- t.destroy();
2432
+ if (t2.width() > maxW) t2.fontSize(Math.max(4, 7 * maxW / t2.width()));
2433
+ if (t2.fontSize() < 4.2) {
2434
+ t2.destroy();
1894
2435
  continue;
1895
2436
  }
1896
- t.offsetX(t.width() / 2);
1897
- t.offsetY(t.height() / 2);
1898
- this.labelGroup.add(t);
2437
+ t2.offsetX(t2.width() / 2);
2438
+ t2.offsetY(t2.height() / 2);
2439
+ this.labelGroup.add(t2);
1899
2440
  if (++count >= MAX_LABELS) break;
1900
2441
  }
1901
2442
  if (this.isoT > 0) this.applyUprightLabels();
@@ -1946,9 +2487,15 @@ var PickerController = class {
1946
2487
  constructor(options) {
1947
2488
  this.renderer = null;
1948
2489
  this._doc = null;
2490
+ /** Section/zone ids hidden from buyers this event (3.3) — seats vanish, not grey. */
2491
+ this.hidden = /* @__PURE__ */ new Set();
1949
2492
  /** label ⇄ id maps — backend speaks labels, the engine speaks ids. */
1950
2493
  this.labelToId = /* @__PURE__ */ new Map();
1951
2494
  this.labelToSeat = /* @__PURE__ */ new Map();
2495
+ /** seatId → chosen ticket-tier id (absent ⇒ the category's first/default tier). */
2496
+ this.seatTiers = /* @__PURE__ */ new Map();
2497
+ /** id → seat, for the section-summary breakdown (renderer members are ids). */
2498
+ this.seatById = /* @__PURE__ */ new Map();
1952
2499
  this.allIds = [];
1953
2500
  // realtime socket
1954
2501
  this.ws = null;
@@ -1966,6 +2513,18 @@ var PickerController = class {
1966
2513
  get doc() {
1967
2514
  return this._doc;
1968
2515
  }
2516
+ /** The chart with hidden sections' seats removed — what buyers actually see. */
2517
+ visibleDoc() {
2518
+ return this._doc ? applyHidden(this._doc, this.hidden) : { objects: [] };
2519
+ }
2520
+ /** Adopt a new hidden set; rebuild the visible chart only if it differs. */
2521
+ syncHidden(ids) {
2522
+ const next = ids.filter((x) => typeof x === "string");
2523
+ if (next.length === this.hidden.size && next.every((id) => this.hidden.has(id))) return false;
2524
+ this.hidden = new Set(next);
2525
+ this.renderer?.setChart(this.visibleDoc());
2526
+ return true;
2527
+ }
1969
2528
  currentHold() {
1970
2529
  return this.hold_;
1971
2530
  }
@@ -1993,15 +2552,19 @@ var PickerController = class {
1993
2552
  this._doc = res.doc;
1994
2553
  this.labelToId = /* @__PURE__ */ new Map();
1995
2554
  this.labelToSeat = /* @__PURE__ */ new Map();
2555
+ this.seatById = /* @__PURE__ */ new Map();
1996
2556
  this.allIds = [];
1997
2557
  for (const s of expandChart(res.doc)) {
1998
2558
  this.labelToId.set(s.label, s.id);
1999
2559
  this.labelToSeat.set(s.label, s);
2560
+ this.seatById.set(s.id, s);
2000
2561
  this.allIds.push(s.id);
2001
2562
  }
2563
+ const currency = res.event.currency ?? this.opts.currency;
2002
2564
  const renderer = createRenderer(host, {
2003
2565
  maxSelection: this.maxSelection,
2004
2566
  confirmSelection: this.opts.confirmSelection,
2567
+ currency,
2005
2568
  onSelect: (seat) => {
2006
2569
  this.opts.onSelect?.(seat);
2007
2570
  this.emitSelectionChange();
@@ -2014,6 +2577,9 @@ var PickerController = class {
2014
2577
  onFocusSeat: this.opts.onFocusSeat,
2015
2578
  onViewChange: this.opts.onViewChange,
2016
2579
  onGAClick: this.opts.onGAClick,
2580
+ // Section tap at the far/zone rung → glide in + surface the summary card.
2581
+ onSectionTap: (id) => this.handleSectionTap(id),
2582
+ onDeckTap: (floorId) => this.handleDeckTap(floorId),
2017
2583
  onFps: this.opts.onFps
2018
2584
  });
2019
2585
  if (this.closed) {
@@ -2021,15 +2587,28 @@ var PickerController = class {
2021
2587
  return null;
2022
2588
  }
2023
2589
  this.renderer = renderer;
2024
- renderer.setChart(res.doc);
2025
- await this.resnapshot();
2590
+ let seats = null;
2591
+ try {
2592
+ const objs = await this.api.objects(this.key);
2593
+ this.hidden = new Set(objs.hidden ?? []);
2594
+ seats = objs.seats;
2595
+ } catch {
2596
+ }
2597
+ if (this.closed) {
2598
+ renderer.destroy();
2599
+ this.renderer = null;
2600
+ return null;
2601
+ }
2602
+ renderer.setChart(this.visibleDoc());
2603
+ if (seats) this.applySeatsMap(seats);
2026
2604
  this.connect();
2027
2605
  return {
2028
2606
  doc: res.doc,
2029
2607
  salesClosed: !!res.event.salesClosed,
2030
2608
  eventName: res.event.name,
2031
2609
  venue: res.event.venue,
2032
- startsAt: res.event.startsAt
2610
+ startsAt: res.event.startsAt,
2611
+ currency
2033
2612
  };
2034
2613
  }
2035
2614
  // ---- selection ------------------------------------------------------------
@@ -2201,6 +2780,126 @@ var PickerController = class {
2201
2780
  setAccessibilityFilter(types) {
2202
2781
  this.renderer?.setAccessibilityFilter?.(types);
2203
2782
  }
2783
+ // ---- multi-floor (Batch 5) ------------------------------------------------
2784
+ /** Floors for the buyer's switcher (>1 ⇒ show it). Single-floor ⇒ one entry. */
2785
+ getFloors() {
2786
+ return this.renderer?.getFloors?.() ?? [];
2787
+ }
2788
+ getActiveFloorId() {
2789
+ return this.renderer?.getActiveFloorId?.() ?? "";
2790
+ }
2791
+ /** Switch the shown floor (2D), then re-apply live seat statuses onto it. */
2792
+ setFloor(id) {
2793
+ const r = this.renderer;
2794
+ if (!r?.setActiveFloor || id === r.getActiveFloorId?.()) return;
2795
+ r.setStacked?.(false);
2796
+ r.setActiveFloor(id);
2797
+ void this.resnapshot();
2798
+ this.emitSelectionChange();
2799
+ }
2800
+ /** 3D all-floors stacked overview ⇄ active floor. Re-applies statuses after. */
2801
+ setStacked(on) {
2802
+ const r = this.renderer;
2803
+ if (!r?.setStacked || on === r.isStacked?.()) return;
2804
+ r.setStacked(on);
2805
+ void this.resnapshot();
2806
+ }
2807
+ isMultiFloor() {
2808
+ return (this._doc?.floors?.length ?? 0) > 1;
2809
+ }
2810
+ /** Tap-a-deck-to-enter: leave the 3D stack, drop to flat 2D on `floorId`, and
2811
+ * tell the host so it can sync its 2D/3D toggle + floor-switcher state. */
2812
+ handleDeckTap(floorId) {
2813
+ const r = this.renderer;
2814
+ if (!r) return;
2815
+ r.setViewMode?.("flat");
2816
+ r.setStacked?.(false);
2817
+ r.setActiveFloor?.(floorId);
2818
+ void this.resnapshot();
2819
+ this.emitSelectionChange();
2820
+ this.opts.onDeckTap?.(floorId);
2821
+ }
2822
+ // ---- big-venue: sections / rungs / projection (Slice 5) -------------------
2823
+ /** Switch the map projection (2D flat ⇄ 3D isometric). No-op on a flat renderer. */
2824
+ setViewMode(mode) {
2825
+ this.renderer?.setViewMode?.(mode);
2826
+ }
2827
+ getViewMode() {
2828
+ return this.renderer?.getViewMode?.() ?? "flat";
2829
+ }
2830
+ /** Current LOD rung (for the ZONES/SECTIONS/SEATS pill). */
2831
+ getRung() {
2832
+ return this.renderer?.getRung?.() ?? "seats";
2833
+ }
2834
+ /** Jump to a rung; ZONES clears any focused summary (back to overview). */
2835
+ setRung(rung) {
2836
+ if (rung === "zones") {
2837
+ this.overview();
2838
+ return;
2839
+ }
2840
+ this.renderer?.setRung?.(rung);
2841
+ }
2842
+ /** Glide in on a section and surface its summary (same path as a section tap). */
2843
+ focusSection(id) {
2844
+ this.handleSectionTap(id);
2845
+ }
2846
+ /** Zoom back out to the whole chart and clear the section-summary card. */
2847
+ overview() {
2848
+ this.renderer?.zoomToFit();
2849
+ this.opts.onSectionFocus?.(null);
2850
+ }
2851
+ /** Glide the camera into a tapped section and emit its computed summary. */
2852
+ handleSectionTap(id) {
2853
+ const r = this.renderer;
2854
+ if (!r) return;
2855
+ r.focusRegion?.(id);
2856
+ this.opts.onSectionFocus?.(this.sectionSummary(id));
2857
+ }
2858
+ /**
2859
+ * Build a section summary from the renderer's spatial membership: section +
2860
+ * zone labels, live seats-left, price range, and the per-category breakdown.
2861
+ */
2862
+ sectionSummary(id) {
2863
+ const r = this.renderer;
2864
+ const doc = this._doc;
2865
+ if (!r || !doc) return null;
2866
+ const sec = doc.objects.find(
2867
+ (o) => o.type === "section" && o.id === id
2868
+ );
2869
+ if (!sec) return null;
2870
+ const memberIds = r.sectionMembers?.(id) ?? [];
2871
+ const byCat = /* @__PURE__ */ new Map();
2872
+ let seatsLeft = 0;
2873
+ for (const sid of memberIds) {
2874
+ const seat = this.seatById.get(sid);
2875
+ if (!seat) continue;
2876
+ byCat.set(seat.categoryKey, (byCat.get(seat.categoryKey) ?? 0) + 1);
2877
+ if (r.getStatus(sid) === "free") seatsLeft++;
2878
+ }
2879
+ const categories = [...byCat.entries()].map(([key, count]) => {
2880
+ const cat = doc.categories.find((c) => c.key === key);
2881
+ return {
2882
+ key,
2883
+ count,
2884
+ label: cat?.label ?? key,
2885
+ color: cat?.color ?? "#6e7bff",
2886
+ price: cat?.price ?? 0
2887
+ };
2888
+ }).sort((a, b) => a.price - b.price);
2889
+ const prices = categories.map((c) => c.price);
2890
+ const zone = sec.zone ? doc.zones?.find((z) => z.id === sec.zone) : void 0;
2891
+ const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
2892
+ return {
2893
+ id,
2894
+ label: sec.label,
2895
+ zoneLabel: zone?.label ?? "",
2896
+ color,
2897
+ seatsLeft,
2898
+ priceMin: prices.length ? prices[0] : 0,
2899
+ priceMax: prices.length ? prices[prices.length - 1] : 0,
2900
+ categories
2901
+ };
2902
+ }
2204
2903
  destroy() {
2205
2904
  this.closed = true;
2206
2905
  if (this.reconnectTimer) {
@@ -2223,11 +2922,53 @@ var PickerController = class {
2223
2922
  }
2224
2923
  // ---- internals ------------------------------------------------------------
2225
2924
  toSeat(s) {
2226
- return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey) };
2925
+ const tiers = this.tiersFor(s.categoryKey);
2926
+ if (!tiers) {
2927
+ return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey) };
2928
+ }
2929
+ const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
2930
+ return {
2931
+ id: s.id,
2932
+ label: s.label,
2933
+ categoryKey: s.categoryKey,
2934
+ price: chosen.price,
2935
+ tiers,
2936
+ tierId: chosen.id
2937
+ };
2227
2938
  }
2228
2939
  priceFor(categoryKey) {
2229
2940
  return this._doc?.categories.find((c) => c.key === categoryKey)?.price ?? 0;
2230
2941
  }
2942
+ tiersFor(categoryKey) {
2943
+ const t2 = this._doc?.categories.find((c) => c.key === categoryKey)?.tiers;
2944
+ return t2 && t2.length ? t2 : void 0;
2945
+ }
2946
+ /**
2947
+ * Choose a ticket tier for a selected seat (e.g. Adult → Child). Re-emits the
2948
+ * selection so the host's cart + the eventual hold carry the new tier + price.
2949
+ * `tierId = null` reverts to the category's first (default) tier. No-op if the
2950
+ * seat's category has no tiers or the id isn't one of them.
2951
+ */
2952
+ setSeatTier(seatId, tierId) {
2953
+ const seat = this.seatById.get(seatId);
2954
+ if (!seat) return;
2955
+ const tiers = this.tiersFor(seat.categoryKey);
2956
+ if (!tiers) return;
2957
+ if (tierId == null) this.seatTiers.delete(seatId);
2958
+ else if (tiers.some((t2) => t2.id === tierId)) this.seatTiers.set(seatId, tierId);
2959
+ else return;
2960
+ this.emitSelectionChange();
2961
+ if (this.hold_) this.hold_ = { ...this.hold_, seats: this.seatsForLabels(this.hold_.labels) };
2962
+ }
2963
+ /** PickerSeats (with chosen tier) for a set of held labels. */
2964
+ seatsForLabels(labels) {
2965
+ const out = [];
2966
+ for (const l of labels) {
2967
+ const s = this.labelToSeat.get(l);
2968
+ if (s) out.push(this.toSeat(s));
2969
+ }
2970
+ return out;
2971
+ }
2231
2972
  emitSelectionChange() {
2232
2973
  this.opts.onSelectionChange?.(this.getSelection());
2233
2974
  }
@@ -2253,11 +2994,12 @@ var PickerController = class {
2253
2994
  }
2254
2995
  /** Set the open hold + (re)arm the server-authoritative expiry timer. */
2255
2996
  setHold(hold) {
2256
- this.hold_ = hold;
2997
+ const full = { ...hold, seats: this.seatsForLabels(hold.labels) };
2998
+ this.hold_ = full;
2257
2999
  if (this.expiryTimer) clearTimeout(this.expiryTimer);
2258
- const ms = Math.max(0, hold.expiresAt - Date.now());
3000
+ const ms = Math.max(0, full.expiresAt - Date.now());
2259
3001
  this.expiryTimer = setTimeout(() => this.onHoldExpired(), ms);
2260
- this.opts.onHold?.(hold);
3002
+ this.opts.onHold?.(full);
2261
3003
  }
2262
3004
  clearHold() {
2263
3005
  this.hold_ = null;
@@ -2321,6 +3063,11 @@ var PickerController = class {
2321
3063
  const r = this.renderer;
2322
3064
  if (!r || !msg || typeof msg !== "object") return;
2323
3065
  const m = msg;
3066
+ if (Array.isArray(m.hidden) && this.syncHidden(m.hidden)) {
3067
+ void this.resnapshot();
3068
+ this.opts.onStatusChange?.();
3069
+ }
3070
+ if (m.type === "hidden") return;
2324
3071
  if (m.seats && typeof m.seats === "object") {
2325
3072
  this.applySeatsMap(m.seats);
2326
3073
  } else if (Array.isArray(m.changes)) {
@@ -2357,21 +3104,67 @@ var PickerController = class {
2357
3104
  }, delay);
2358
3105
  }
2359
3106
  };
3107
+
3108
+ // src/i18n/bundles.ts
3109
+ var LOADERS = {
3110
+ es: () => import("./es-UM4ZS357.js").then((m) => ({ default: m.es })),
3111
+ de: () => import("./de-Y3E6YTGP.js").then((m) => ({ default: m.de })),
3112
+ fr: () => import("./fr-IGRD2M4W.js").then((m) => ({ default: m.fr }))
3113
+ };
3114
+ var loaded = /* @__PURE__ */ new Set(["en"]);
3115
+ async function loadLocale(code) {
3116
+ const locale = resolveLocale(code);
3117
+ if (locale === "en" || loaded.has(locale)) {
3118
+ setLocale(locale);
3119
+ return locale;
3120
+ }
3121
+ try {
3122
+ const mod = await LOADERS[locale]();
3123
+ setLocale(locale, mod.default);
3124
+ loaded.add(locale);
3125
+ return locale;
3126
+ } catch {
3127
+ setLocale("en");
3128
+ return "en";
3129
+ }
3130
+ }
2360
3131
  export {
2361
3132
  ACCESSIBILITY_TYPES,
2362
3133
  CHART_STORAGE_KEY,
3134
+ DEFAULT_CURRENCY,
2363
3135
  PickerController,
3136
+ SUPPORTED_LOCALES,
2364
3137
  SeatmapRenderer,
3138
+ UNGROUPED_ID,
2365
3139
  accessibilityMeta,
3140
+ allObjects,
3141
+ applyHidden,
2366
3142
  chartBounds,
3143
+ computeSections,
2367
3144
  createRenderer,
3145
+ currencySymbol,
2368
3146
  expandBooth,
2369
3147
  expandChart,
2370
3148
  expandRow,
2371
3149
  expandRowSlots,
2372
3150
  expandTable,
3151
+ floorObjects,
3152
+ floorsOf,
3153
+ formatDate,
3154
+ formatMoney,
3155
+ getLocale,
3156
+ hiddenObjectIds,
3157
+ isSectionHidden,
2373
3158
  layerOf,
3159
+ loadLocale,
2374
3160
  objectCenter,
2375
- pointInPolygon
3161
+ pointInPolygon,
3162
+ resolveLocale,
3163
+ setLocale,
3164
+ setMoneyLocale,
3165
+ setStringOverrides,
3166
+ stackFloors,
3167
+ t,
3168
+ tCount
2376
3169
  };
2377
3170
  //# sourceMappingURL=index.js.map