@seatlayer/js 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -254,10 +254,27 @@ interface EmbeddedDesignerOptions {
254
254
  * Only used when `showLoadingState` is enabled.
255
255
  */
256
256
  loadingTimeoutMs?: number;
257
+ /**
258
+ * How to size the iframe's height. The Designer is a full application (its
259
+ * shell is `position:fixed; height:100dvh`), not flowing content, so it should
260
+ * fill the viewport rather than be measured.
261
+ *
262
+ * - `'fill'` (default): grow the iframe so its bottom edge reaches the bottom
263
+ * of the viewport — `window.innerHeight - iframe.top`, clamped to `minHeight`
264
+ * — recomputed (rAF-throttled) on `resize` / `orientationchange` / `scroll`.
265
+ * The legacy `seatlayer.designer.resize` message is ignored in this mode: it
266
+ * is circular, because the fixed-position shell just echoes the iframe height.
267
+ * - a number: a fixed pixel height. In this mode the legacy resize message is
268
+ * still honoured (unless `autoResize` is `false`) so older hosts keep growing.
269
+ */
270
+ height?: 'fill' | number;
271
+ /** Minimum height (px) that `'fill'` mode clamps to. Defaults to `480`. */
272
+ minHeight?: number;
257
273
  /**
258
274
  * Auto-grow the iframe to the height the Designer reports over the resize
259
- * protocol (`seatlayer.designer.resize`). Defaults to `true`. Set `false` when
260
- * the host sizes the iframe itself (e.g. a fixed-height chrome).
275
+ * protocol (`seatlayer.designer.resize`). Only applies when `height` is a fixed
276
+ * number; ignored in `'fill'` mode. Defaults to `true`. Set `false` when the
277
+ * host sizes a fixed-height iframe itself.
261
278
  */
262
279
  autoResize?: boolean;
263
280
  /**
@@ -289,6 +306,8 @@ declare class EmbeddedDesigner {
289
306
  private fsKeyHandler;
290
307
  /** Latest height (px string) the Designer reported; re-applied after unpin. */
291
308
  private lastAutoHeight;
309
+ private fillRaf;
310
+ private fillListening;
292
311
  constructor(options: EmbeddedDesignerOptions);
293
312
  mount(): HTMLIFrameElement;
294
313
  /** Replace the iframe instead of assigning a new fragment to an existing one. */
@@ -297,6 +316,18 @@ declare class EmbeddedDesigner {
297
316
  destroy(): void;
298
317
  private loadingStateEnabled;
299
318
  private autoResizeEnabled;
319
+ /** Fill mode is the default; a numeric `height` opts into a fixed pixel box. */
320
+ private fillEnabled;
321
+ /**
322
+ * Size the iframe so its bottom edge meets the bottom of the viewport
323
+ * (`window.innerHeight - top`), clamped to `minHeight`. No-op while pinned
324
+ * fullscreen (the pin fills the viewport itself).
325
+ */
326
+ private applyFill;
327
+ /** rAF-throttled fill recompute, so a burst of scroll/resize ticks coalesces. */
328
+ private scheduleFill;
329
+ private startFill;
330
+ private stopFill;
300
331
  /**
301
332
  * Pin the iframe over the host page as a viewport-filling overlay. We save the
302
333
  * iframe's inline style and the document scroll state so `unpinFullscreen`
@@ -325,7 +356,7 @@ declare class EmbeddedDesigner {
325
356
  * SeatPicker — the full buyer experience as a widget.
326
357
  *
327
358
  * Where `SeatingChart` is canvas-only, SeatPicker owns the complete chrome
328
- * from the canonical UX (SeatmapUX/11 Buyer Picker.dc.html): branded header,
359
+ * from the canonical UX contract: branded header,
329
360
  * live price panel, selection tray with GA steppers, hold countdown, snipe
330
361
  * toasts and expiry recovery — all on top of the shared PickerController, so
331
362
  * every host gets the whole experience with one mount.
package/dist/index.d.ts CHANGED
@@ -254,10 +254,27 @@ interface EmbeddedDesignerOptions {
254
254
  * Only used when `showLoadingState` is enabled.
255
255
  */
256
256
  loadingTimeoutMs?: number;
257
+ /**
258
+ * How to size the iframe's height. The Designer is a full application (its
259
+ * shell is `position:fixed; height:100dvh`), not flowing content, so it should
260
+ * fill the viewport rather than be measured.
261
+ *
262
+ * - `'fill'` (default): grow the iframe so its bottom edge reaches the bottom
263
+ * of the viewport — `window.innerHeight - iframe.top`, clamped to `minHeight`
264
+ * — recomputed (rAF-throttled) on `resize` / `orientationchange` / `scroll`.
265
+ * The legacy `seatlayer.designer.resize` message is ignored in this mode: it
266
+ * is circular, because the fixed-position shell just echoes the iframe height.
267
+ * - a number: a fixed pixel height. In this mode the legacy resize message is
268
+ * still honoured (unless `autoResize` is `false`) so older hosts keep growing.
269
+ */
270
+ height?: 'fill' | number;
271
+ /** Minimum height (px) that `'fill'` mode clamps to. Defaults to `480`. */
272
+ minHeight?: number;
257
273
  /**
258
274
  * Auto-grow the iframe to the height the Designer reports over the resize
259
- * protocol (`seatlayer.designer.resize`). Defaults to `true`. Set `false` when
260
- * the host sizes the iframe itself (e.g. a fixed-height chrome).
275
+ * protocol (`seatlayer.designer.resize`). Only applies when `height` is a fixed
276
+ * number; ignored in `'fill'` mode. Defaults to `true`. Set `false` when the
277
+ * host sizes a fixed-height iframe itself.
261
278
  */
262
279
  autoResize?: boolean;
263
280
  /**
@@ -289,6 +306,8 @@ declare class EmbeddedDesigner {
289
306
  private fsKeyHandler;
290
307
  /** Latest height (px string) the Designer reported; re-applied after unpin. */
291
308
  private lastAutoHeight;
309
+ private fillRaf;
310
+ private fillListening;
292
311
  constructor(options: EmbeddedDesignerOptions);
293
312
  mount(): HTMLIFrameElement;
294
313
  /** Replace the iframe instead of assigning a new fragment to an existing one. */
@@ -297,6 +316,18 @@ declare class EmbeddedDesigner {
297
316
  destroy(): void;
298
317
  private loadingStateEnabled;
299
318
  private autoResizeEnabled;
319
+ /** Fill mode is the default; a numeric `height` opts into a fixed pixel box. */
320
+ private fillEnabled;
321
+ /**
322
+ * Size the iframe so its bottom edge meets the bottom of the viewport
323
+ * (`window.innerHeight - top`), clamped to `minHeight`. No-op while pinned
324
+ * fullscreen (the pin fills the viewport itself).
325
+ */
326
+ private applyFill;
327
+ /** rAF-throttled fill recompute, so a burst of scroll/resize ticks coalesces. */
328
+ private scheduleFill;
329
+ private startFill;
330
+ private stopFill;
300
331
  /**
301
332
  * Pin the iframe over the host page as a viewport-filling overlay. We save the
302
333
  * iframe's inline style and the document scroll state so `unpinFullscreen`
@@ -325,7 +356,7 @@ declare class EmbeddedDesigner {
325
356
  * SeatPicker — the full buyer experience as a widget.
326
357
  *
327
358
  * Where `SeatingChart` is canvas-only, SeatPicker owns the complete chrome
328
- * from the canonical UX (SeatmapUX/11 Buyer Picker.dc.html): branded header,
359
+ * from the canonical UX contract: branded header,
329
360
  * live price panel, selection tray with GA steppers, hold countdown, snipe
330
361
  * toasts and expiry recovery — all on top of the shared PickerController, so
331
362
  * every host gets the whole experience with one mount.
package/dist/index.js CHANGED
@@ -327,6 +327,7 @@ var TYPES = /* @__PURE__ */ new Set([
327
327
  "seatlayer.designer.error"
328
328
  ]);
329
329
  var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
330
+ var DEFAULT_MIN_FILL_HEIGHT = 480;
330
331
  function resolveContainer2(container) {
331
332
  if (typeof container !== "string") return container;
332
333
  const element = document.querySelector(container);
@@ -374,12 +375,23 @@ var EmbeddedDesigner = class {
374
375
  this.fsKeyHandler = null;
375
376
  /** Latest height (px string) the Designer reported; re-applied after unpin. */
376
377
  this.lastAutoHeight = "";
378
+ // Viewport-fill sizing: pending rAF handle + whether listeners are attached.
379
+ this.fillRaf = null;
380
+ this.fillListening = false;
381
+ /** rAF-throttled fill recompute, so a burst of scroll/resize ticks coalesces. */
382
+ this.scheduleFill = () => {
383
+ if (this.fillRaf !== null) return;
384
+ this.fillRaf = requestAnimationFrame(() => {
385
+ this.fillRaf = null;
386
+ this.applyFill();
387
+ });
388
+ };
377
389
  this.handleMessage = (event) => {
378
390
  if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
379
391
  if (!event.data || typeof event.data !== "object") return;
380
392
  const data = event.data;
381
393
  if (data.type === "seatlayer.designer.resize") {
382
- if (this.autoResizeEnabled() && typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
394
+ if (!this.fillEnabled() && this.autoResizeEnabled() && typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
383
395
  this.lastAutoHeight = `${Math.round(data.px)}px`;
384
396
  if (!this.pinned) this.frame.style.height = this.lastAutoHeight;
385
397
  }
@@ -437,11 +449,11 @@ var EmbeddedDesigner = class {
437
449
  this.designerOrigin = url.origin;
438
450
  const frame = document.createElement("iframe");
439
451
  frame.title = this.options.title ?? "Venue chart Designer";
440
- frame.allow = this.options.allow ?? "clipboard-write";
452
+ frame.allow = this.options.allow ?? "fullscreen; clipboard-write";
441
453
  frame.referrerPolicy = this.options.referrerPolicy ?? "origin";
442
454
  frame.src = url.toString();
443
455
  frame.style.width = "100%";
444
- frame.style.height = "100%";
456
+ frame.style.height = typeof this.options.height === "number" ? `${this.options.height}px` : "100%";
445
457
  frame.style.border = "0";
446
458
  Object.assign(frame.style, this.options.style);
447
459
  if (this.options.className) frame.className = this.options.className;
@@ -449,6 +461,7 @@ var EmbeddedDesigner = class {
449
461
  window.addEventListener("message", this.handleMessage);
450
462
  container.append(frame);
451
463
  this.frame = frame;
464
+ if (this.fillEnabled()) this.startFill();
452
465
  this.phase = "loading";
453
466
  if (this.loadingStateEnabled()) {
454
467
  this.ensureContainerPositioned(container);
@@ -472,6 +485,7 @@ var EmbeddedDesigner = class {
472
485
  }
473
486
  destroy() {
474
487
  window.removeEventListener("message", this.handleMessage);
488
+ this.stopFill();
475
489
  this.unpinFullscreen();
476
490
  this.clearTimeoutTimer();
477
491
  this.removeOverlay();
@@ -488,6 +502,41 @@ var EmbeddedDesigner = class {
488
502
  autoResizeEnabled() {
489
503
  return this.options.autoResize !== false;
490
504
  }
505
+ /** Fill mode is the default; a numeric `height` opts into a fixed pixel box. */
506
+ fillEnabled() {
507
+ return typeof this.options.height !== "number";
508
+ }
509
+ /**
510
+ * Size the iframe so its bottom edge meets the bottom of the viewport
511
+ * (`window.innerHeight - top`), clamped to `minHeight`. No-op while pinned
512
+ * fullscreen (the pin fills the viewport itself).
513
+ */
514
+ applyFill() {
515
+ if (!this.frame || this.pinned) return;
516
+ const min = this.options.minHeight ?? DEFAULT_MIN_FILL_HEIGHT;
517
+ const top = this.frame.getBoundingClientRect().top;
518
+ const target = Math.max(min, Math.round(window.innerHeight - top));
519
+ this.frame.style.height = `${target}px`;
520
+ }
521
+ startFill() {
522
+ this.applyFill();
523
+ if (this.fillListening) return;
524
+ this.fillListening = true;
525
+ window.addEventListener("resize", this.scheduleFill);
526
+ window.addEventListener("orientationchange", this.scheduleFill);
527
+ window.addEventListener("scroll", this.scheduleFill, { passive: true });
528
+ }
529
+ stopFill() {
530
+ if (this.fillRaf !== null) {
531
+ cancelAnimationFrame(this.fillRaf);
532
+ this.fillRaf = null;
533
+ }
534
+ if (!this.fillListening) return;
535
+ this.fillListening = false;
536
+ window.removeEventListener("resize", this.scheduleFill);
537
+ window.removeEventListener("orientationchange", this.scheduleFill);
538
+ window.removeEventListener("scroll", this.scheduleFill);
539
+ }
491
540
  /**
492
541
  * Pin the iframe over the host page as a viewport-filling overlay. We save the
493
542
  * iframe's inline style and the document scroll state so `unpinFullscreen`
@@ -526,7 +575,8 @@ var EmbeddedDesigner = class {
526
575
  if (this.frame) {
527
576
  if (this.frameStyleBeforeFs === null) this.frame.removeAttribute("style");
528
577
  else this.frame.setAttribute("style", this.frameStyleBeforeFs);
529
- if (this.autoResizeEnabled() && this.lastAutoHeight) this.frame.style.height = this.lastAutoHeight;
578
+ if (this.fillEnabled()) this.applyFill();
579
+ else if (this.autoResizeEnabled() && this.lastAutoHeight) this.frame.style.height = this.lastAutoHeight;
530
580
  }
531
581
  this.frameStyleBeforeFs = null;
532
582
  if (this.docOverflowBeforeFs !== null) {
@@ -838,6 +888,11 @@ var CSS = `
838
888
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-filtersec,
839
889
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-filters,
840
890
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-prices-sec{display:none}
891
+ /* Reclaim the bottom sheet once the cart has anything: the "Find best seats"
892
+ panel collapses too. EXCEPT the confirm ("Replace your current choices?") and
893
+ in-flight busy states, which legitimately show with a non-empty cart \u2014 those
894
+ set data-ba-active="true" (see setAttribute alongside data-has-selection). */
895
+ .sl-picker[data-layout="narrow"][data-has-selection="true"]:not([data-ba-active="true"]) .sl-ba{display:none}
841
896
  /* touch chrome: pinch-zoom exists \u2014 hide +/\u2212 on the sheet layout (keep fit) */
842
897
  .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zin"],
843
898
  .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zout"]{display:none}
@@ -2688,6 +2743,7 @@ var SeatPicker = class _SeatPicker {
2688
2743
  if (narrow) {
2689
2744
  card.className = "sl-seccard strip on";
2690
2745
  card.setAttribute("role", "status");
2746
+ card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
2691
2747
  card.innerHTML = `<span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span><span class="sl-seccard-left">${leftLabel}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn;
2692
2748
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
2693
2749
  (this.els.sheetHead ?? this.els.side ?? this.els.map).appendChild(card);
@@ -3098,7 +3154,7 @@ var SeatPicker = class _SeatPicker {
3098
3154
  } else if (!seats.length && !heldItems.length) {
3099
3155
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
3100
3156
  }
3101
- const noPicks = !seats.length && !heldItems.length;
3157
+ const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
3102
3158
  if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
3103
3159
  const cats = this.controller.doc?.categories ?? [];
3104
3160
  parts.push(this.bestAvailableConfirm ? `<div class="sl-ba" role="alert"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Replace your current choices?</div><div class="sl-ba-replace"><b>We\u2019ll find ${this.baQty} seats together.</b><span>Your manually selected tickets will be removed only after a new group is secured.</span></div><div class="sl-ba-actions"><button type="button" data-ba-cancel>Keep mine</button><button type="button" class="replace" data-ba-replace>Find new seats</button></div></div>` : `<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Find the best seats together</div><div class="sl-ba-copy"><span class="wide">We\u2019ll choose the closest available group for you.</span><span class="narrow">Closest available group, chosen instantly.</span></div>` + (cats.length > 1 ? `<select aria-label="Preferred ticket type" data-ba-cat><option value="">Any ticket type</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`);
@@ -3242,6 +3298,10 @@ var SeatPicker = class _SeatPicker {
3242
3298
  this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
3243
3299
  this.els.total.textContent = count ? this.money(total) : "";
3244
3300
  this.root?.setAttribute("data-has-selection", String(count > 0));
3301
+ this.root?.setAttribute(
3302
+ "data-ba-active",
3303
+ String(this.bestAvailableConfirm || this.bestAvailableBusy)
3304
+ );
3245
3305
  this.els.foot?.classList.toggle("empty", count === 0);
3246
3306
  if (this.els.seatSummary) {
3247
3307
  this.els.seatSummary.textContent = count ? `${count} selected` : "";